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 🔌
"🗄️ 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"

View File

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

View File

@@ -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"]:

View File

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

View File

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

View File

@@ -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", {})

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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