Backup automatico script del 2026-07-12 07:00

This commit is contained in:
2026-07-12 07:00:03 +02:00
parent b5a2a814e2
commit 927f0d4184
18 changed files with 2365 additions and 422 deletions
+337 -146
View File
@@ -25,11 +25,17 @@ def setup_logger() -> logging.Logger:
logger.setLevel(logging.INFO)
logger.handlers.clear()
fh = RotatingFileHandler(LOG_FILE, maxBytes=1_000_000, backupCount=5, encoding="utf-8")
fh.setLevel(logging.DEBUG)
fmt = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
fh.setFormatter(fmt)
logger.addHandler(fh)
log_path = os.environ.get("ROAD_WEATHER_LOG", LOG_FILE)
try:
fh = RotatingFileHandler(log_path, maxBytes=1_000_000, backupCount=5, encoding="utf-8")
fh.setLevel(logging.DEBUG)
fh.setFormatter(fmt)
logger.addHandler(fh)
except OSError:
sh = logging.StreamHandler()
sh.setFormatter(fmt)
logger.addHandler(sh)
return logger
@@ -121,14 +127,43 @@ WEATHER_CODES = {
# =============================================================================
def get_google_maps_api_key() -> Optional[str]:
"""Ottiene la chiave API di Google Maps da variabile d'ambiente."""
"""Ottiene la chiave API di Google Maps da variabile d'ambiente o file condiviso."""
api_key = os.environ.get('GOOGLE_MAPS_API_KEY', '').strip()
if api_key:
return api_key
api_key = os.environ.get('GOOGLE_API_KEY', '').strip()
if api_key:
return api_key
# Debug: verifica tutte le variabili d'ambiente che contengono GOOGLE
for path in (
os.environ.get("GOOGLE_MAPS_API_KEY_FILE", ""),
"/etc/google_maps_api_key",
os.path.expanduser("~/.google_maps_api_key"),
os.path.join(SCRIPT_DIR, ".env"),
):
if not path or not os.path.isfile(path):
continue
try:
if path.endswith(".env"):
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line.startswith("GOOGLE_MAPS_API_KEY="):
v = line.split("=", 1)[1].strip().strip("'\"")
if v:
return v
elif line.startswith("GOOGLE_API_KEY="):
v = line.split("=", 1)[1].strip().strip("'\"")
if v:
return v
else:
with open(path, "r", encoding="utf-8") as f:
v = f.read().strip()
if v:
return v
except OSError:
continue
if os.environ.get('DEBUG_GOOGLE_MAPS', ''):
google_vars = {k: v[:10] + '...' if len(v) > 10 else v for k, v in os.environ.items() if 'GOOGLE' in k.upper()}
LOGGER.debug(f"Variabili GOOGLE trovate: {google_vars}")
@@ -433,7 +468,7 @@ def get_weather_data(lat: float, lon: float, model_slug: str) -> Optional[Dict]:
url = f"https://api.open-meteo.com/v1/forecast"
# Parametri base (aggiunto soil_temperature_0cm per analisi ghiaccio più accurata)
hourly_params = "temperature_2m,relative_humidity_2m,precipitation,rain,showers,snowfall,weathercode,visibility,wind_speed_10m,wind_gusts_10m,soil_temperature_0cm,dew_point_2m"
hourly_params = "temperature_2m,relative_humidity_2m,precipitation,rain,showers,snowfall,snow_depth,weathercode,visibility,wind_speed_10m,wind_gusts_10m,soil_temperature_0cm,dew_point_2m"
# Aggiungi CAPE se disponibile (AROME Seamless o ICON)
if model_slug in ["meteofrance_seamless", "italia_meteo_arpae_icon_2i", "icon_eu"]:
@@ -627,6 +662,242 @@ def analyze_past_24h_conditions(weather_data: Dict) -> Dict:
}
def summarize_point_weather(weather_data: Dict) -> Dict:
"""Manto nevoso attuale e precipitazioni previste 12h/24h per un punto."""
result = {
"snow_depth_cm": None,
"rain_12h_mm": 0.0,
"rain_24h_mm": 0.0,
"snow_12h_cm": 0.0,
"snow_24h_cm": 0.0,
}
if not weather_data or "hourly" not in weather_data:
return result
hourly = weather_data["hourly"]
times = hourly.get("time", [])
if not times:
return result
now = datetime.datetime.now(datetime.timezone.utc)
rain = hourly.get("rain", [])
snowfall = hourly.get("snowfall", [])
snow_depth = hourly.get("snow_depth", [])
timestamps = []
for ts_str in times:
try:
if "Z" in ts_str:
ts = datetime.datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
else:
ts = datetime.datetime.fromisoformat(ts_str)
if ts.tzinfo is None:
ts = ts.replace(tzinfo=datetime.timezone.utc)
timestamps.append(ts)
except Exception:
continue
latest_depth_cm = None
for i, ts in enumerate(timestamps):
if i < len(snow_depth) and snow_depth[i] is not None and ts <= now:
latest_depth_cm = float(snow_depth[i]) * 100.0
if ts < now:
continue
hours_ahead = (ts - now).total_seconds() / 3600.0
if hours_ahead >= 24:
continue
r = rain[i] if i < len(rain) and rain[i] is not None else 0.0
snow = snowfall[i] if i < len(snowfall) and snowfall[i] is not None else 0.0
if hours_ahead < 12:
result["rain_12h_mm"] += float(r)
result["snow_12h_cm"] += float(snow)
result["rain_24h_mm"] += float(r)
result["snow_24h_cm"] += float(snow)
if latest_depth_cm is not None and latest_depth_cm >= 0.05:
result["snow_depth_cm"] = round(latest_depth_cm, 1)
result["rain_12h_mm"] = round(result["rain_12h_mm"], 1)
result["rain_24h_mm"] = round(result["rain_24h_mm"], 1)
result["snow_12h_cm"] = round(result["snow_12h_cm"], 1)
result["snow_24h_cm"] = round(result["snow_24h_cm"], 1)
return result
RISK_BADGE_MAP = {
"neve": ("❄️", "Neve"),
"gelicidio": ("🔴🔴", "Gelicidio"),
"ghiaccio": ("🔴", "Ghiaccio"),
"brina": ("🟡", "Brina"),
"pioggia": ("🌧️", "Pioggia"),
"temporale": ("⛈️", "Temporale"),
"vento": ("💨", "Vento"),
"nebbia": ("🌫️", "Nebbia"),
"grandine": ("🌨️", "Grandine"),
"nessuno": ("", "Nessun rischio"),
}
def _first_dict(series):
"""Prende il primo valore non-nullo (dict) da una serie pandas."""
for val in series:
if val is not None and (isinstance(val, dict) or (isinstance(val, str) and val != "")):
return val
return {}
def _aggregate_route_points(df: pd.DataFrame):
"""Raggruppa punti percorso con rischi effettivi e meteo riassuntivo."""
max_risk_per_point = df.groupby("point_index").agg({
"max_risk_level": "max",
"point_name": "first",
"past_24h": _first_dict,
"weather_summary": _first_dict,
}).sort_values("point_index")
seen_names = {}
unique_indices = []
for idx, row in max_risk_per_point.iterrows():
point_name = row["point_name"]
name_key = point_name.split("(")[0].strip()
past_24h = row.get("past_24h", {}) if isinstance(row.get("past_24h"), dict) else {}
has_snow_ice = past_24h.get("snow_present") or past_24h.get("ice_persistence_likely")
if name_key not in seen_names:
seen_names[name_key] = idx
unique_indices.append(idx)
else:
existing_idx = seen_names[name_key]
existing_row = max_risk_per_point.loc[existing_idx]
existing_past_24h = existing_row.get("past_24h", {}) if isinstance(existing_row.get("past_24h"), dict) else {}
existing_has_snow_ice = existing_past_24h.get("snow_present") or existing_past_24h.get("ice_persistence_likely")
if row["max_risk_level"] > existing_row["max_risk_level"]:
unique_indices.remove(existing_idx)
seen_names[name_key] = idx
unique_indices.append(idx)
elif row["max_risk_level"] == existing_row["max_risk_level"] and has_snow_ice and not existing_has_snow_ice:
unique_indices.remove(existing_idx)
seen_names[name_key] = idx
unique_indices.append(idx)
max_risk_per_point = max_risk_per_point.loc[unique_indices]
effective_risk_levels_dict = {}
for idx, row in max_risk_per_point.iterrows():
level = int(row["max_risk_level"])
past_24h = row.get("past_24h", {}) if isinstance(row.get("past_24h"), dict) else {}
if level == 0 and past_24h:
if past_24h.get("snow_present"):
level = 4
elif past_24h.get("ice_persistence_likely"):
level = 2
effective_risk_levels_dict[idx] = level
max_risk_per_point = max_risk_per_point.copy()
max_risk_per_point["effective_risk_level"] = max_risk_per_point.index.map(effective_risk_levels_dict)
risks_per_point = {}
for _, row in df[df["max_risk_level"] > 0].iterrows():
point_idx = row["point_index"]
if point_idx not in risks_per_point:
risks_per_point[point_idx] = {}
risk_type = row["risk_type"]
risk_level = row["risk_level"]
risk_desc = row["risk_description"]
if risk_type not in risks_per_point[point_idx] or risks_per_point[point_idx][risk_type]["level"] < risk_level:
risks_per_point[point_idx][risk_type] = {
"type": risk_type,
"desc": risk_desc,
"level": risk_level,
}
for idx, row in max_risk_per_point.iterrows():
effective_risk = row.get("effective_risk_level", 0)
max_risk = int(row["max_risk_level"])
if effective_risk > 0 and max_risk == 0:
if idx not in risks_per_point:
risks_per_point[idx] = {}
past_24h = row.get("past_24h", {}) if isinstance(row.get("past_24h"), dict) else {}
if effective_risk >= 4:
risk_type, risk_desc = "neve", "Neve presente"
elif effective_risk == 2:
risk_type = "ghiaccio"
min_temp = past_24h.get("min_temp_2m")
hours_below_2c = past_24h.get("hours_below_2c", 0)
if min_temp is not None:
risk_desc = f"Ghiaccio persistente (Tmin: {min_temp:.1f}°C, {hours_below_2c}h <2°C)"
else:
risk_desc = "Ghiaccio persistente"
elif effective_risk == 1:
risk_type = "brina"
min_temp = past_24h.get("min_temp_2m")
risk_desc = f"Brina possibile (Tmin: {min_temp:.1f}°C)" if min_temp is not None else "Brina possibile"
else:
continue
risks_per_point[idx][risk_type] = {"type": risk_type, "desc": risk_desc, "level": effective_risk}
return max_risk_per_point, risks_per_point
def _primary_risk_for_point(risks: Dict, effective_risk: int) -> Tuple[str, str, List[str]]:
"""Ritorna (badge, label, descrizioni) per il punto."""
type_order = ["neve", "gelicidio", "ghiaccio", "brina", "temporale", "pioggia", "vento", "nebbia", "grandine"]
risk_list = sorted(risks.values(), key=lambda x: x.get("level", 0), reverse=True) if risks else []
descriptions = [r.get("desc", "") for r in risk_list if r.get("desc")]
primary_type = risk_list[0]["type"] if risk_list else "nessuno"
for t in type_order:
if t in risks:
primary_type = t
break
if effective_risk >= 4 and "neve" not in risks:
primary_type = "neve"
elif effective_risk == 3 and primary_type == "nessuno":
primary_type = "gelicidio"
elif effective_risk == 2 and primary_type in ("nessuno", "brina"):
primary_type = "ghiaccio"
elif effective_risk == 1 and primary_type == "nessuno":
primary_type = "brina"
badge, label = RISK_BADGE_MAP.get(primary_type, ("⚠️", primary_type.capitalize()))
return badge, label, descriptions
def build_route_points_table(df: pd.DataFrame) -> List[Dict]:
"""Tabella punti notevoli per WebApp."""
if df.empty:
return []
max_risk_per_point, risks_per_point = _aggregate_route_points(df)
points = []
for idx, row in max_risk_per_point.iterrows():
effective_risk = int(row.get("effective_risk_level", 0))
risks = risks_per_point.get(idx, {})
badge, label, descriptions = _primary_risk_for_point(risks, effective_risk)
weather = row.get("weather_summary", {}) if isinstance(row.get("weather_summary"), dict) else {}
points.append({
"name": row["point_name"],
"risk": {
"badge": badge,
"label": label,
"descriptions": descriptions[:4],
"level": effective_risk,
},
"snow_depth_cm": weather.get("snow_depth_cm"),
"precip": {
"rain_12h_mm": weather.get("rain_12h_mm", 0.0),
"rain_24h_mm": weather.get("rain_24h_mm", 0.0),
"snow_12h_cm": weather.get("snow_12h_cm", 0.0),
"snow_24h_cm": weather.get("snow_24h_cm", 0.0),
},
})
return points
# =============================================================================
# ANALISI RISCHI METEO
# =============================================================================
@@ -1098,12 +1369,14 @@ def analyze_route_weather_risks(city1: str, city2: str, model_slug: Optional[str
'risk_description': 'Dati meteo non disponibili',
'risk_value': 0.0,
'max_risk_level': 0,
'point_name': point_name
'point_name': point_name,
'weather_summary': {},
})
continue
# Analizza condizioni 24h precedenti
past_24h = analyze_past_24h_conditions(weather_data)
weather_summary = summarize_point_weather(weather_data)
# Analizza rischi (passa anche past_24h per analisi temporale evolutiva)
risk_analysis = analyze_weather_risks(weather_data, model_slug, hours_ahead=24, past_24h_info=past_24h)
@@ -1121,7 +1394,8 @@ def analyze_route_weather_risks(city1: str, city2: str, model_slug: Optional[str
'risk_value': 0.0,
'max_risk_level': 0,
'point_name': point_name,
'past_24h': past_24h # Aggiungi analisi 24h precedenti anche se nessun rischio
'past_24h': past_24h,
'weather_summary': weather_summary,
})
continue
@@ -1161,7 +1435,8 @@ def analyze_route_weather_risks(city1: str, city2: str, model_slug: Optional[str
'risk_value': risk.get("value", 0.0),
'max_risk_level': hour_data["max_risk_level"],
'point_name': point_name,
'past_24h': past_24h
'past_24h': past_24h,
'weather_summary': weather_summary,
})
else:
all_results.append({
@@ -1175,7 +1450,8 @@ def analyze_route_weather_risks(city1: str, city2: str, model_slug: Optional[str
'risk_value': 0.0,
'max_risk_level': 0,
'point_name': point_name,
'past_24h': past_24h
'past_24h': past_24h,
'weather_summary': weather_summary,
})
if not all_results:
@@ -1193,141 +1469,13 @@ def format_route_weather_report(df: pd.DataFrame, city1: str, city2: str) -> str
"""Formatta report compatto dei rischi meteo lungo percorso."""
if df.empty:
return "❌ Nessun dato disponibile per il percorso."
# Raggruppa per punto e trova rischio massimo + analisi 24h
# Usa funzione custom per past_24h per assicurarsi che venga preservato correttamente
def first_dict(series):
"""Prende il primo valore non-nullo, utile per dict."""
for val in series:
if val is not None and (isinstance(val, dict) or (isinstance(val, str) and val != '')):
return val
return {}
max_risk_per_point = df.groupby('point_index').agg({
'max_risk_level': 'max',
'point_name': 'first',
'past_24h': first_dict # Usa funzione custom per preservare dict
}).sort_values('point_index')
# Rimuovi duplicati per nome (punti con stesso nome ma indici diversi)
# Considera anche neve/ghiaccio persistente nella scelta
seen_names = {}
unique_indices = []
for idx, row in max_risk_per_point.iterrows():
point_name = row['point_name']
# Normalizza nome (rimuovi suffissi tra parentesi)
name_key = point_name.split('(')[0].strip()
past_24h = row.get('past_24h', {}) if isinstance(row.get('past_24h'), dict) else {}
has_snow_ice = past_24h.get('snow_present') or past_24h.get('ice_persistence_likely')
if name_key not in seen_names:
seen_names[name_key] = idx
unique_indices.append(idx)
else:
# Se duplicato, mantieni quello con rischio maggiore O con neve/ghiaccio
existing_idx = seen_names[name_key]
existing_row = max_risk_per_point.loc[existing_idx]
existing_past_24h = existing_row.get('past_24h', {}) if isinstance(existing_row.get('past_24h'), dict) else {}
existing_has_snow_ice = existing_past_24h.get('snow_present') or existing_past_24h.get('ice_persistence_likely')
# Priorità: rischio maggiore, oppure neve/ghiaccio se rischio uguale
if row['max_risk_level'] > existing_row['max_risk_level']:
unique_indices.remove(existing_idx)
seen_names[name_key] = idx
unique_indices.append(idx)
elif row['max_risk_level'] == existing_row['max_risk_level'] and has_snow_ice and not existing_has_snow_ice:
# Stesso rischio, ma questo ha neve/ghiaccio
unique_indices.remove(existing_idx)
seen_names[name_key] = idx
unique_indices.append(idx)
# Filtra solo punti unici
max_risk_per_point = max_risk_per_point.loc[unique_indices]
# Calcola effective_risk_level per ogni punto UNICO (considerando persistenza)
effective_risk_levels_dict = {}
for idx, row in max_risk_per_point.iterrows():
level = int(row['max_risk_level'])
past_24h = row.get('past_24h', {}) if isinstance(row.get('past_24h'), dict) else {}
# Se livello è 0, verifica persistenza per assegnare livello appropriato
if level == 0 and past_24h:
if past_24h.get('snow_present'):
level = 4 # Neve presente
elif past_24h.get('ice_persistence_likely'):
# Se ice_persistence_likely è True, significa che c'è ghiaccio persistente
# (calcolato in analyze_past_24h_conditions basandosi su suolo gelato,
# precipitazioni con temperature basse, o neve presente)
# Quindi deve essere classificato come ghiaccio (livello 2), non brina
level = 2 # Ghiaccio persistente
effective_risk_levels_dict[idx] = level
# Aggiungi effective_risk_level al DataFrame
max_risk_per_point['effective_risk_level'] = max_risk_per_point.index.map(effective_risk_levels_dict)
# Trova rischi unici per ogni punto (raggruppa per tipo, mantieni solo il più grave)
risks_per_point = {}
# Prima aggiungi rischi futuri (max_risk_level > 0)
for idx, row in df[df['max_risk_level'] > 0].iterrows():
point_idx = row['point_index']
if point_idx not in risks_per_point:
risks_per_point[point_idx] = {}
risk_type = row['risk_type']
risk_level = row['risk_level']
risk_desc = row['risk_description']
# Raggruppa per tipo di rischio, mantieni solo quello con livello più alto
if risk_type not in risks_per_point[point_idx] or risks_per_point[point_idx][risk_type]['level'] < risk_level:
risks_per_point[point_idx][risk_type] = {
'type': risk_type,
'desc': risk_desc,
'level': risk_level
}
# Poi aggiungi punti con persistenza ma senza rischi futuri (max_risk_level == 0 ma effective_risk > 0)
for idx, row in max_risk_per_point.iterrows():
effective_risk = row.get('effective_risk_level', 0)
max_risk = int(row['max_risk_level'])
# Se ha persistenza ma non rischi futuri, aggiungi rischio basato su persistenza
if effective_risk > 0 and max_risk == 0:
if idx not in risks_per_point:
risks_per_point[idx] = {}
past_24h = row.get('past_24h', {}) if isinstance(row.get('past_24h'), dict) else {}
# Determina tipo di rischio basandosi su effective_risk_level
if effective_risk >= 4:
risk_type = 'neve'
risk_desc = "Neve presente"
elif effective_risk == 2:
risk_type = 'ghiaccio'
# Determina descrizione basandosi su condizioni
min_temp = past_24h.get('min_temp_2m')
hours_below_2c = past_24h.get('hours_below_2c', 0)
if min_temp is not None:
risk_desc = f"Ghiaccio persistente (Tmin: {min_temp:.1f}°C, {hours_below_2c}h <2°C)"
else:
risk_desc = "Ghiaccio persistente"
elif effective_risk == 1:
risk_type = 'brina'
min_temp = past_24h.get('min_temp_2m')
if min_temp is not None:
risk_desc = f"Brina possibile (Tmin: {min_temp:.1f}°C)"
else:
risk_desc = "Brina possibile"
else:
continue # Skip se non abbiamo un tipo valido
# Aggiungi al dict rischi (usa idx come chiave, non point_idx)
risks_per_point[idx][risk_type] = {
'type': risk_type,
'desc': risk_desc,
'level': effective_risk
}
max_risk_per_point, risks_per_point = _aggregate_route_points(df)
effective_risk_levels_dict = {
idx: int(row["effective_risk_level"])
for idx, row in max_risk_per_point.iterrows()
}
# Verifica se la chiave Google Maps è disponibile
api_key_available = get_google_maps_api_key() is not None
@@ -1820,3 +1968,46 @@ def generate_route_weather_map(df: pd.DataFrame, city1: str, city2: str, output_
LOGGER.error(f"Errore salvataggio mappa: {e}")
plt.close(fig)
return False
def build_road_json(city1: str, city2: str, maps_dir: str) -> Dict:
"""Analisi percorso stradale per WebApp."""
if not PANDAS_AVAILABLE:
return {"error": "pandas/numpy non disponibili sul server"}
df = analyze_route_weather_risks(city1, city2, model_slug=None)
if df is None or df.empty:
return {"error": f"Impossibile analizzare il percorso {city1}{city2}"}
report = format_route_weather_report(df, city1, city2)
os.makedirs(maps_dir, exist_ok=True)
import hashlib
key = hashlib.sha256(f"{city1}|{city2}".encode()).hexdigest()[:16]
map_name = f"road_{key}.png"
map_path = os.path.join(maps_dir, map_name)
map_ok = generate_route_weather_map(df, city1, city2, map_path)
plain = report.replace("*", "").replace("_", "").replace("`", "")
return {
"from": city1,
"to": city2,
"report": plain,
"map_id": map_name if map_ok else None,
"points": build_route_points_table(df),
}
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Road weather analysis")
parser.add_argument("city1")
parser.add_argument("city2")
parser.add_argument("--json", action="store_true")
parser.add_argument("--maps-dir", default="/tmp")
args = parser.parse_args()
if args.json:
print(json.dumps(build_road_json(args.city1, args.city2, args.maps_dir), ensure_ascii=False))
else:
if not PANDAS_AVAILABLE:
raise SystemExit("pandas richiesto")
df = analyze_route_weather_risks(args.city1, args.city2)
print(format_route_weather_report(df, args.city1, args.city2))