Backup automatico script del 2026-01-25 07:00

This commit is contained in:
2026-01-25 07:00:03 +01:00
parent 9dbe0cfa93
commit f0c5672607
16 changed files with 241 additions and 61 deletions

3
scripts/pi2-backup/super_watchdog.sh Normal file → Executable file
View File

@@ -36,9 +36,8 @@ TARGETS=(
# INFRASTRUTTURA 🔌 # INFRASTRUTTURA 🔌
"🗄️ NAS DS214|192.168.128.90" "🗄️ NAS DS214|192.168.128.90"
"🔌 Switch Sala (.105)|192.168.128.105" "🔌 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 Lavanderia (.107)|192.168.128.107"
"🔌 Switch Taverna (.108)|192.168.128.108"
# WIFI 📶 # WIFI 📶
"📶 WiFi Sala (.101)|192.168.128.101" "📶 WiFi Sala (.101)|192.168.128.101"

View File

@@ -15,6 +15,7 @@ from zoneinfo import ZoneInfo
import requests import requests
from dateutil import parser from dateutil import parser
from open_meteo_client import configure_open_meteo_session
# ============================================================================= # =============================================================================
# arome_snow_alert.py # 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" params["minutely_15"] = "snowfall,precipitation_probability,precipitation,rain,temperature_2m"
try: 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: if r.status_code == 400:
# Se 400 e abbiamo minutely_15, riprova senza # Se 400 e abbiamo minutely_15, riprova senza
if "minutely_15" in params and model == MODEL_AROME: 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() params_no_minutely = params.copy()
del params_no_minutely["minutely_15"] del params_no_minutely["minutely_15"]
try: 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: if r2.status_code == 200:
return r2.json() return r2.json()
except Exception: except Exception:
@@ -408,7 +409,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str,
params_no_minutely = params.copy() params_no_minutely = params.copy()
del params_no_minutely["minutely_15"] del params_no_minutely["minutely_15"]
try: 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: if r2.status_code == 200:
return r2.json() return r2.json()
except Exception: except Exception:
@@ -460,7 +461,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str,
params_no_minutely = params.copy() params_no_minutely = params.copy()
del params_no_minutely["minutely_15"] del params_no_minutely["minutely_15"]
try: 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: if r2.status_code == 200:
return r2.json() return r2.json()
except Exception: except Exception:
@@ -474,7 +475,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str,
params_no_minutely = params.copy() params_no_minutely = params.copy()
del params_no_minutely["minutely_15"] del params_no_minutely["minutely_15"]
try: 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: if r2.status_code == 200:
return r2.json() return r2.json()
except Exception: except Exception:
@@ -488,7 +489,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str,
params_no_minutely = params.copy() params_no_minutely = params.copy()
del params_no_minutely["minutely_15"] del params_no_minutely["minutely_15"]
try: 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: if r2.status_code == 200:
return r2.json() return r2.json()
except Exception: except Exception:
@@ -1399,6 +1400,7 @@ def analyze_snow(chat_ids: Optional[List[str]] = None, debug_mode: bool = False)
casa_location_name = "🏠 Casa" casa_location_name = "🏠 Casa"
with requests.Session() as session: with requests.Session() as session:
configure_open_meteo_session(session, headers=HTTP_HEADERS)
for p in POINTS: for p in POINTS:
# Recupera AROME seamless # Recupera AROME seamless
data_arome = get_forecast(session, p["lat"], p["lon"], MODEL_AROME) data_arome = get_forecast(session, p["lat"], p["lon"], MODEL_AROME)

View File

@@ -5,6 +5,7 @@ import datetime
import requests import requests
import shlex import shlex
import json import json
import time
from functools import wraps from functools import wraps
from typing import Optional from typing import Optional
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
@@ -58,9 +59,8 @@ INFRA_DEVICES = [
{"name": "📶 WiFi Taverna", "ip": "192.168.128.103"}, {"name": "📶 WiFi Taverna", "ip": "192.168.128.103"},
{"name": "📶 WiFi Dado", "ip": "192.168.128.104"}, {"name": "📶 WiFi Dado", "ip": "192.168.128.104"},
{"name": "🔌 Sw Sala", "ip": "192.168.128.105"}, {"name": "🔌 Sw Sala", "ip": "192.168.128.105"},
{"name": "🔌 Sw Main", "ip": "192.168.128.106"}, {"name": "🔌 Sw Tav", "ip": "192.168.128.106"},
{"name": "🔌 Sw Lav", "ip": "192.168.128.107"}, {"name": "🔌 Sw Lav", "ip": "192.168.128.107"}
{"name": "🔌 Sw Tav", "ip": "192.168.128.108"}
] ]
logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO) logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO)
@@ -84,9 +84,17 @@ def get_ping_icon(ip):
except Exception: return "🔴" except Exception: return "🔴"
def get_device_stats(device): 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) 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] uptime = uptime_raw.replace("up ", "").split(", load")[0].split(", ")[0]
temp = "N/A" temp = "N/A"
if dtype in ["pi", "local"]: if dtype in ["pi", "local"]:

View File

@@ -1,5 +1,6 @@
import argparse import argparse
import requests import requests
from open_meteo_client import open_meteo_get
import datetime import datetime
import os import os
import sys import sys
@@ -77,31 +78,68 @@ def get_bot_token():
sys.exit(1) sys.exit(1)
def save_current_state(state): def save_current_state(state, report_meta=None):
try: try:
# Aggiungi timestamp corrente per tracciare quando è stato salvato lo stato # Aggiungi timestamp corrente per tracciare quando è stato salvato lo stato
if report_meta is None:
report_meta = {}
state_with_meta = { state_with_meta = {
"points": state, "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: with open(STATE_FILE, 'w') as f:
json.dump(state_with_meta, f) json.dump(state_with_meta, f)
except Exception as e: except Exception as e:
print(f"Errore salvataggio stato: {e}") print(f"Errore salvataggio stato: {e}")
def load_previous_state(): def load_state_with_meta():
if not os.path.exists(STATE_FILE): if not os.path.exists(STATE_FILE):
return {} return {}, {}
try: try:
with open(STATE_FILE, 'r') as f: with open(STATE_FILE, 'r') as f:
data = json.load(f) data = json.load(f)
# Supporta sia il formato vecchio (solo dict di punti) che nuovo (con metadata) # Supporta sia il formato vecchio (solo dict di punti) che nuovo (con metadata)
if isinstance(data, dict) and "points" in data: if isinstance(data, dict) and "points" in data:
return data["points"] return data.get("points", {}), data.get("report_meta", {})
else: else:
return data return data, {}
except Exception: 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: 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" params["minutely_15"] = "temperature_2m,precipitation,rain,snowfall"
try: try:
response = requests.get(url, params=params, timeout=15) response = open_meteo_get(url, params=params, timeout=(5, 15))
response.raise_for_status() response.raise_for_status()
return response.json() return response.json()
except Exception as e: 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" url = "https://geocoding-api.open-meteo.com/v1/search"
try: 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", []) res = resp.json().get("results", [])
if res: if res:
res = res[0] res = res[0]
@@ -1926,7 +1964,8 @@ def main():
DEBUG_MODE = args.debug DEBUG_MODE = args.debug
token = get_bot_token() 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 = {} current_state = {}
new_alerts = [] new_alerts = []
@@ -2074,7 +2113,8 @@ def main():
for detail in past_24h_details: for detail in past_24h_details:
final_msg += f"{detail}\n" 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) # 3. Rischio Cessato (Tutti i modelli danno verde)
# IMPORTANTE: Non inviare "allerta rientrata" se ci sono ancora condizioni che mantengono il ghiaccio # 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 # Non aggiungere a solved_alerts - il ghiaccio potrebbe ancora essere presente
# Ma potremmo inviare un messaggio informativo se in debug mode # Ma potremmo inviare un messaggio informativo se in debug mode
if 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: else:
# Condizioni completamente risolte: neve sciolta e temperature sopra lo zero # Condizioni completamente risolte: neve sciolta e temperature sopra lo zero
if DEBUG_MODE: if DEBUG_MODE:
@@ -2153,7 +2194,8 @@ def main():
solved_msg += f" (Scioglimento confermato: {', '.join(melting_info)})" solved_msg += f" (Scioglimento confermato: {', '.join(melting_info)})"
else: else:
solved_msg += " (Tutti i modelli)" 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) # 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: elif max_risk_level < old_level and max_risk_level > 0:
@@ -2191,7 +2233,8 @@ def main():
for detail in past_24h_details: for detail in past_24h_details:
improvement_msg += f"{detail}\n" 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 # Genera e invia mappa solo quando ci sono aggiornamenti
if new_alerts or solved_alerts: if new_alerts or solved_alerts:
@@ -2231,7 +2274,7 @@ def main():
print("Nessuna variazione.") print("Nessuna variazione.")
if not DEBUG_MODE: if not DEBUG_MODE:
save_current_state(current_state) save_current_state(current_state, report_meta=report_meta)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -13,6 +13,7 @@ from typing import Dict, List, Optional, Tuple
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
import requests import requests
from open_meteo_client import open_meteo_get
from dateutil import parser from dateutil import parser
# ============================================================================= # =============================================================================
@@ -236,7 +237,7 @@ def get_forecast() -> Optional[Dict]:
"minutely_15": "temperature_2m", # Dettaglio 15 minuti per inizio preciso gelo "minutely_15": "temperature_2m", # Dettaglio 15 minuti per inizio preciso gelo
} }
try: 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: if r.status_code == 400:
try: try:
j = r.json() j = r.json()

View File

@@ -9,6 +9,7 @@ import time
from typing import Optional, List from typing import Optional, List
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from dateutil import parser as date_parser from dateutil import parser as date_parser
from open_meteo_client import open_meteo_get
# Setup logging # Setup logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 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): def get_coordinates(city_name: str):
params = {"name": city_name, "count": 1, "language": "it", "format": "json"} params = {"name": city_name, "count": 1, "language": "it", "format": "json"}
try: 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() data = r.json()
if "results" in data and data["results"]: if "results" in data and data["results"]:
res = data["results"][0] res = data["results"][0]
@@ -223,7 +224,7 @@ def get_forecast(lat, lon, model=None, is_home=False, timezone=None, retry_after
try: try:
t0 = time.time() t0 = time.time()
# Timeout ridotto a 20s per fallire più velocemente in caso di problemi # 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: if r.status_code != 200:
# Dettagli errore più specifici # Dettagli errore più specifici
error_details = f"Status {r.status_code}" error_details = f"Status {r.status_code}"
@@ -276,7 +277,7 @@ def get_visibility_forecast(lat, lon):
try: try:
t0 = time.time() t0 = time.time()
# Timeout ridotto a 12s per fallire più velocemente # 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: if r.status_code == 200:
data = r.json() data = r.json()
hourly = data.get("hourly", {}) hourly = data.get("hourly", {})
@@ -299,7 +300,7 @@ def get_visibility_forecast(lat, lon):
try: try:
t0 = time.time() t0 = time.time()
# Timeout ridotto a 12s per fallire più velocemente # 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: if r.status_code == 200:
data = r.json() data = r.json()
hourly = data.get("hourly", {}) hourly = data.get("hourly", {})

View File

@@ -1,4 +1,5 @@
import argparse import argparse
import datetime
import subprocess import subprocess
import re import re
import os import os
@@ -21,6 +22,16 @@ LIMIT_JITTER = 30.0 # ms di deviazione (sopra 30ms lagga la voce/gioco)
# File di stato # File di stato
STATE_FILE = "/home/daniely/docker/telegram-bot/quality_state.json" 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): 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): def measure_quality(chat_ids: Optional[List[str]] = None):
print("--- Avvio Test Qualità Linea ---") print("--- Avvio Test Qualità Linea ---")
log_line("INFO Avvio Test Qualità Linea")
# Esegue 50 ping rapidi (0.2s intervallo) # Esegue 50 ping rapidi (0.2s intervallo)
# -q: quiet (solo riepilogo finale) # -q: quiet (solo riepilogo finale)
@@ -89,7 +101,9 @@ def measure_quality(chat_ids: Optional[List[str]] = None):
else: else:
avg_ping = 0.0 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 --- # --- LOGICA ALLARME ---
state = load_state() state = load_state()
@@ -110,8 +124,10 @@ def measure_quality(chat_ids: Optional[List[str]] = None):
send_telegram(msg, chat_ids=chat_ids) send_telegram(msg, chat_ids=chat_ids)
save_state(True) save_state(True)
print("Allarme inviato.") print("Allarme inviato.")
log_line("WARN Allarme inviato")
else: else:
print("Qualità ancora scarsa (già notificato).") print("Qualità ancora scarsa (già notificato).")
log_line("WARN Qualità ancora scarsa (già notificato)")
elif was_active and not is_bad: elif was_active and not is_bad:
# RECOVERY # RECOVERY
@@ -121,8 +137,10 @@ def measure_quality(chat_ids: Optional[List[str]] = None):
send_telegram(msg, chat_ids=chat_ids) send_telegram(msg, chat_ids=chat_ids)
save_state(False) save_state(False)
print("Recovery inviata.") print("Recovery inviata.")
log_line("INFO Recovery inviata")
else: else:
print("Linea OK.") print("Linea OK.")
log_line("INFO Linea OK")
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Network quality monitor") parser = argparse.ArgumentParser(description="Network quality monitor")

View File

@@ -13,6 +13,7 @@ from zoneinfo import ZoneInfo
import requests import requests
from dateutil import parser from dateutil import parser
from open_meteo_client import open_meteo_get
# ========================= # =========================
# CONFIG # 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" params["minutely_15"] = "snowfall,precipitation_probability,precipitation,rain,temperature_2m,wind_speed_10m,wind_direction_10m"
try: 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: if r.status_code == 400:
# Se 400 e abbiamo minutely_15, riprova senza # Se 400 e abbiamo minutely_15, riprova senza
if "minutely_15" in params and model == MODEL_AROME: 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() params_no_minutely = params.copy()
del params_no_minutely["minutely_15"] del params_no_minutely["minutely_15"]
try: 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: if r2.status_code == 200:
return r2.json() return r2.json()
except Exception: except Exception:
@@ -227,7 +228,7 @@ def get_forecast(model: str, use_minutely: bool = True, forecast_days: int = 2)
params_no_minutely = params.copy() params_no_minutely = params.copy()
del params_no_minutely["minutely_15"] del params_no_minutely["minutely_15"]
try: 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: if r2.status_code == 200:
return r2.json() return r2.json()
except Exception: except Exception:
@@ -260,7 +261,7 @@ def get_forecast(model: str, use_minutely: bool = True, forecast_days: int = 2)
params_no_minutely = params.copy() params_no_minutely = params.copy()
del params_no_minutely["minutely_15"] del params_no_minutely["minutely_15"]
try: 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: if r2.status_code == 200:
return r2.json() return r2.json()
except Exception: except Exception:
@@ -274,7 +275,7 @@ def get_forecast(model: str, use_minutely: bool = True, forecast_days: int = 2)
params_no_minutely = params.copy() params_no_minutely = params.copy()
del params_no_minutely["minutely_15"] del params_no_minutely["minutely_15"]
try: 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: if r2.status_code == 200:
return r2.json() return r2.json()
except Exception: except Exception:
@@ -288,7 +289,7 @@ def get_forecast(model: str, use_minutely: bool = True, forecast_days: int = 2)
params_no_minutely = params.copy() params_no_minutely = params.copy()
del params_no_minutely["minutely_15"] del params_no_minutely["minutely_15"]
try: 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: if r2.status_code == 200:
return r2.json() return r2.json()
except Exception: except Exception:

View File

@@ -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)

View File

@@ -12,6 +12,7 @@ from zoneinfo import ZoneInfo
from collections import defaultdict, Counter, Counter from collections import defaultdict, Counter, Counter
from typing import List, Dict, Tuple, Optional from typing import List, Dict, Tuple, Optional
from statistics import mean, median from statistics import mean, median
from open_meteo_client import open_meteo_get
# --- CONFIGURAZIONE DEFAULT --- # --- CONFIGURAZIONE DEFAULT ---
DEFAULT_LAT = 43.9356 DEFAULT_LAT = 43.9356
@@ -94,7 +95,7 @@ def get_coordinates(query):
return DEFAULT_LAT, DEFAULT_LON, DEFAULT_NAME, "SM" return DEFAULT_LAT, DEFAULT_LON, DEFAULT_NAME, "SM"
url = "https://geocoding-api.open-meteo.com/v1/search" url = "https://geocoding-api.open-meteo.com/v1/search"
try: 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", []) res = resp.json().get("results", [])
if res: if res:
res = res[0] 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 "timezone": timezone if timezone else TZ_STR, "forecast_days": min(forecast_days, 3) # Limita a 3 giorni per modelli ad alta risoluzione
} }
try: try:
resp = requests.get(url, params=params, timeout=20) resp = open_meteo_get(url, params=params, timeout=(5, 20))
if resp.status_code == 200: if resp.status_code == 200:
data = resp.json() data = resp.json()
# Converti snow_depth da metri a cm per tutti i modelli (Open-Meteo restituisce in metri) # 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 "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: try:
resp = requests.get(url, params=params, timeout=20) resp = open_meteo_get(url, params=params, timeout=(5, 20))
if resp.status_code == 200: if resp.status_code == 200:
data = resp.json() data = resp.json()
# Converti snow_depth da metri a cm per tutti i modelli (Open-Meteo restituisce in metri) # 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 "timezone": timezone if timezone else TZ_STR, "models": model, "forecast_days": forecast_days
} }
try: try:
resp = requests.get(url, params=params, timeout=25) resp = open_meteo_get(url, params=params, timeout=(5, 25))
if resp.status_code == 200: if resp.status_code == 200:
data = resp.json() data = resp.json()
# Converti snow_depth da metri a cm per tutti i modelli (Open-Meteo restituisce in metri) # Converti snow_depth da metri a cm per tutti i modelli (Open-Meteo restituisce in metri)

View File

@@ -14,6 +14,7 @@ import requests
import time import time
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
from typing import Dict, List, Tuple, Optional from typing import Dict, List, Tuple, Optional
from open_meteo_client import open_meteo_get
# Setup logging # Setup logging
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 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" url = "https://geocoding-api.open-meteo.com/v1/search"
params = {"name": city_name, "count": 1, "language": "it"} params = {"name": city_name, "count": 1, "language": "it"}
try: try:
resp = requests.get(url, params=params, timeout=5) resp = open_meteo_get(url, params=params, timeout=(5, 10))
if resp.status_code == 200: if resp.status_code == 200:
data = resp.json() data = resp.json()
if data.get("results"): if data.get("results"):
@@ -449,7 +450,7 @@ def get_weather_data(lat: float, lon: float, model_slug: str) -> Optional[Dict]:
} }
try: try:
resp = requests.get(url, params=params, timeout=10) resp = open_meteo_get(url, params=params, timeout=(5, 10))
if resp.status_code == 200: if resp.status_code == 200:
data = resp.json() data = resp.json()
# Verifica che snowfall sia presente nei dati # Verifica che snowfall sia presente nei dati

View File

@@ -14,6 +14,7 @@ from zoneinfo import ZoneInfo
import requests import requests
from dateutil import parser from dateutil import parser
from open_meteo_client import open_meteo_get
# ============================================================================= # =============================================================================
# SEVERE WEATHER ALERT (next 48h) - Casa (LAT/LON) # 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 use_minutely = True
try: 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: if r.status_code == 400:
# Se 400 e abbiamo minutely_15, riprova senza # Se 400 e abbiamo minutely_15, riprova senza
if use_minutely and "minutely_15" in params: 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() params_no_minutely = params.copy()
del params_no_minutely["minutely_15"] del params_no_minutely["minutely_15"]
try: 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: if r2.status_code == 200:
return r2.json() return r2.json()
except Exception: except Exception:
@@ -329,7 +330,7 @@ def fetch_forecast(models_value: str, lat: Optional[float] = None, lon: Optional
params_no_minutely = params.copy() params_no_minutely = params.copy()
del params_no_minutely["minutely_15"] del params_no_minutely["minutely_15"]
try: 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: if r2.status_code == 200:
return r2.json() return r2.json()
except Exception: except Exception:
@@ -361,7 +362,7 @@ def fetch_forecast(models_value: str, lat: Optional[float] = None, lon: Optional
params_no_minutely = params.copy() params_no_minutely = params.copy()
del params_no_minutely["minutely_15"] del params_no_minutely["minutely_15"]
try: 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: if r2.status_code == 200:
return r2.json() return r2.json()
except Exception: except Exception:
@@ -375,7 +376,7 @@ def fetch_forecast(models_value: str, lat: Optional[float] = None, lon: Optional
params_no_minutely = params.copy() params_no_minutely = params.copy()
del params_no_minutely["minutely_15"] del params_no_minutely["minutely_15"]
try: 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: if r2.status_code == 200:
return r2.json() return r2.json()
except Exception: except Exception:

View File

@@ -14,6 +14,7 @@ from zoneinfo import ZoneInfo
import requests import requests
from dateutil import parser from dateutil import parser
from open_meteo_client import open_meteo_get
# ============================================================================= # =============================================================================
# SEVERE WEATHER ALERT CIRCONDARIO (next 48h) - Analisi Temporali Severi # 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 params["hourly"] += ",cape" # ICON potrebbe avere CAPE
try: 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: if r.status_code == 400:
try: try:
j = r.json() j = r.json()

View File

@@ -19,6 +19,7 @@ from zoneinfo import ZoneInfo
import requests import requests
from dateutil import parser from dateutil import parser
from open_meteo_client import open_meteo_get
# ============================================================================= # =============================================================================
# CONFIGURATION # CONFIGURATION
@@ -278,7 +279,7 @@ def fetch_soil_and_weather(lat: float, lon: float, timezone: str = TZ) -> Option
} }
try: 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: if r.status_code == 400:
try: try:
j = r.json() j = r.json()
@@ -331,7 +332,7 @@ def fetch_weather_only(lat: float, lon: float, timezone: str = TZ) -> Optional[D
} }
try: 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() r.raise_for_status()
return r.json() return r.json()
except Exception as e: except Exception as e:
@@ -1495,6 +1496,12 @@ def main():
lon = args.lon if args.lon is not None else DEFAULT_LON lon = args.lon if args.lon is not None else DEFAULT_LON
location = args.location if args.location else DEFAULT_LOCATION_NAME location = args.location if args.location else DEFAULT_LOCATION_NAME
timezone = args.timezone if args.timezone else TZ 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 # Determina modalità operativa
force_send = args.force or args.debug force_send = args.force or args.debug

View File

@@ -13,6 +13,7 @@ from zoneinfo import ZoneInfo
import requests import requests
from dateutil import parser from dateutil import parser
from open_meteo_client import configure_open_meteo_session
# ============================================================================= # =============================================================================
# snow_radar.py # snow_radar.py
@@ -199,7 +200,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float) -> Optional[
} }
try: 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: if r.status_code == 400:
try: try:
j = r.json() j = r.json()
@@ -642,6 +643,7 @@ def main(chat_ids: Optional[List[str]] = None, debug_mode: bool = False, chat_id
# Analizza località predefinite # Analizza località predefinite
LOGGER.info("Analisi %d località predefinite...", len(LOCATIONS)) LOGGER.info("Analisi %d località predefinite...", len(LOCATIONS))
with requests.Session() as session: with requests.Session() as session:
configure_open_meteo_session(session, headers=HTTP_HEADERS)
results = [] results = []
for i, loc in enumerate(LOCATIONS): for i, loc in enumerate(LOCATIONS):
# Calcola distanza da San Marino # Calcola distanza da San Marino

View File

@@ -14,6 +14,7 @@ from zoneinfo import ZoneInfo
import requests import requests
from dateutil import parser from dateutil import parser
from open_meteo_client import configure_open_meteo_session
# ============================================================================= # =============================================================================
# student_alert.py # student_alert.py
@@ -222,7 +223,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str)
if model == MODEL_AROME: if model == MODEL_AROME:
params["minutely_15"] = "precipitation,rain,snowfall,precipitation_probability,temperature_2m" params["minutely_15"] = "precipitation,rain,snowfall,precipitation_probability,temperature_2m"
try: 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: if r.status_code == 400:
# Se 400 e abbiamo minutely_15, riprova senza # Se 400 e abbiamo minutely_15, riprova senza
if "minutely_15" in params and model == MODEL_AROME: 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() params_no_minutely = params.copy()
del params_no_minutely["minutely_15"] del params_no_minutely["minutely_15"]
try: 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: if r2.status_code == 200:
return r2.json() return r2.json()
except Exception: except Exception:
@@ -243,7 +244,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str)
params_no_minutely = params.copy() params_no_minutely = params.copy()
del params_no_minutely["minutely_15"] del params_no_minutely["minutely_15"]
try: 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: if r2.status_code == 200:
return r2.json() return r2.json()
except Exception: except Exception:
@@ -275,7 +276,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str)
params_no_minutely = params.copy() params_no_minutely = params.copy()
del params_no_minutely["minutely_15"] del params_no_minutely["minutely_15"]
try: 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: if r2.status_code == 200:
return r2.json() return r2.json()
except Exception: except Exception:
@@ -289,7 +290,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str)
params_no_minutely = params.copy() params_no_minutely = params.copy()
del params_no_minutely["minutely_15"] del params_no_minutely["minutely_15"]
try: 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: if r2.status_code == 200:
return r2.json() return r2.json()
except Exception: except Exception:
@@ -303,7 +304,7 @@ def get_forecast(session: requests.Session, lat: float, lon: float, model: str)
params_no_minutely = params.copy() params_no_minutely = params.copy()
del params_no_minutely["minutely_15"] del params_no_minutely["minutely_15"]
try: 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: if r2.status_code == 200:
return r2.json() return r2.json()
except Exception: 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 comparisons: Dict[str, Dict] = {} # point_name -> comparison info
with requests.Session() as session: with requests.Session() as session:
configure_open_meteo_session(session, headers=HTTP_HEADERS)
# Trigger: Bologna # Trigger: Bologna
bo = POINTS[0] bo = POINTS[0]
bo_data_arome = get_forecast(session, bo["lat"], bo["lon"], MODEL_AROME) bo_data_arome = get_forecast(session, bo["lat"], bo["lon"], MODEL_AROME)