Aggiornamento script meteo (Vigilia di Natale)
This commit is contained in:
@@ -1,209 +1,483 @@
|
|||||||
import requests
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import time
|
import html
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from dateutil import parser
|
import time
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
from typing import Dict, List, Optional
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
# --- CONFIGURAZIONE UTENTE ---
|
import requests
|
||||||
# 👇👇 INSERISCI QUI I TUOI DATI 👇👇
|
from dateutil import parser
|
||||||
TELEGRAM_BOT_TOKEN = "8155587974:AAF9OekvBpixtk8ZH6KoIc0L8edbhdXt7A4"
|
|
||||||
TELEGRAM_CHAT_IDS = ["64463169", "24827341", "132455422", "5405962012"]
|
|
||||||
|
|
||||||
# --- PUNTI DI MONITORAGGIO ---
|
# =============================================================================
|
||||||
# Sostituito San Leo con Carpegna
|
# arome_snow_alert.py
|
||||||
|
#
|
||||||
|
# Scopo:
|
||||||
|
# Monitorare neve prevista nelle prossime 24 ore su più punti (Casa/Titano/Dogana/Carpegna)
|
||||||
|
# e notificare su Telegram se:
|
||||||
|
# - esiste almeno 1 ora nelle prossime 24h con snowfall > 0.2 cm
|
||||||
|
# (nessuna persistenza richiesta)
|
||||||
|
#
|
||||||
|
# Modello meteo:
|
||||||
|
# SOLO AROME HD 1.5 km (Meteo-France): meteofrance_arome_france_hd
|
||||||
|
#
|
||||||
|
# Token Telegram:
|
||||||
|
# Nessun token in chiaro. Lettura in ordine:
|
||||||
|
# 1) env TELEGRAM_BOT_TOKEN
|
||||||
|
# 2) ~/.telegram_dpc_bot_token
|
||||||
|
# 3) /etc/telegram_dpc_bot_token
|
||||||
|
#
|
||||||
|
# Debug:
|
||||||
|
# DEBUG=1 python3 arome_snow_alert.py
|
||||||
|
#
|
||||||
|
# Log:
|
||||||
|
# ./arome_snow_alert.log (stessa cartella dello script)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
DEBUG = os.environ.get("DEBUG", "0").strip() == "1"
|
||||||
|
|
||||||
|
# ----------------- TELEGRAM -----------------
|
||||||
|
TELEGRAM_CHAT_IDS = ["64463169", "24827341", "132455422", "5405962012"]
|
||||||
|
TOKEN_FILE_HOME = os.path.expanduser("~/.telegram_dpc_bot_token")
|
||||||
|
TOKEN_FILE_ETC = "/etc/telegram_dpc_bot_token"
|
||||||
|
|
||||||
|
# ----------------- PUNTI DI MONITORAGGIO -----------------
|
||||||
POINTS = [
|
POINTS = [
|
||||||
{"name": "🏠 Casa", "lat": 43.9356, "lon": 12.4296},
|
{"name": "🏠 Casa", "lat": 43.9356, "lon": 12.4296},
|
||||||
{"name": "⛰️ Titano", "lat": 43.9360, "lon": 12.4460},
|
{"name": "⛰️ Titano", "lat": 43.9360, "lon": 12.4460},
|
||||||
{"name": "🏢 Dogana", "lat": 43.9800, "lon": 12.4900},
|
{"name": "🏢 Dogana", "lat": 43.9800, "lon": 12.4900},
|
||||||
{"name": "🏔️ Carpegna", "lat": 43.7819, "lon": 12.3346}
|
{"name": "🏔️ Carpegna", "lat": 43.7819, "lon": 12.3346},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Soglia notifica (cm)
|
# ----------------- LOGICA ALLERTA -----------------
|
||||||
SOGLIA_NOTIFICA = 0.0
|
TZ = "Europe/Rome"
|
||||||
|
TZINFO = ZoneInfo(TZ)
|
||||||
|
|
||||||
# File di stato per ricordare l'ultima allerta
|
HOURS_AHEAD = 24
|
||||||
|
|
||||||
|
# Soglia precipitazione neve oraria (cm/h): NOTIFICA se qualsiasi ora > soglia
|
||||||
|
SNOW_HOURLY_THRESHOLD_CM = 0.2
|
||||||
|
|
||||||
|
# Stagione invernale: 1 Nov -> 15 Apr
|
||||||
|
WINTER_START_MONTH = 11
|
||||||
|
WINTER_END_MONTH = 4
|
||||||
|
WINTER_END_DAY = 15
|
||||||
|
|
||||||
|
# File di stato
|
||||||
STATE_FILE = "/home/daniely/docker/telegram-bot/snow_state.json"
|
STATE_FILE = "/home/daniely/docker/telegram-bot/snow_state.json"
|
||||||
|
|
||||||
# --- FUNZIONI DI UTILITÀ ---
|
# ----------------- OPEN-METEO -----------------
|
||||||
|
OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"
|
||||||
|
HTTP_HEADERS = {"User-Agent": "rpi-arome-snow-alert/2.1"}
|
||||||
|
MODEL = "meteofrance_arome_france_hd"
|
||||||
|
|
||||||
def is_winter_season():
|
# ----------------- LOG FILE -----------------
|
||||||
"""Ritorna True se oggi è tra il 1 Novembre e il 15 Aprile"""
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
now = datetime.datetime.now()
|
LOG_FILE = os.path.join(BASE_DIR, "arome_snow_alert.log")
|
||||||
month = now.month
|
|
||||||
day = now.day
|
|
||||||
|
|
||||||
if month >= 11: return True # Nov, Dic
|
|
||||||
if month <= 3: return True # Gen, Feb, Mar
|
def setup_logger() -> logging.Logger:
|
||||||
if month == 4 and day <= 15: return True # Fino al 15 Apr
|
logger = logging.getLogger("arome_snow_alert")
|
||||||
|
logger.setLevel(logging.DEBUG if DEBUG else 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)
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
sh = logging.StreamHandler()
|
||||||
|
sh.setLevel(logging.DEBUG)
|
||||||
|
sh.setFormatter(fmt)
|
||||||
|
logger.addHandler(sh)
|
||||||
|
|
||||||
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
LOGGER = setup_logger()
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Utility
|
||||||
|
# =============================================================================
|
||||||
|
def now_local() -> datetime.datetime:
|
||||||
|
return datetime.datetime.now(TZINFO)
|
||||||
|
|
||||||
|
|
||||||
|
def is_winter_season() -> bool:
|
||||||
|
"""True se oggi è tra 1 Novembre e 15 Aprile (in TZ locale)."""
|
||||||
|
now = now_local()
|
||||||
|
m = now.month
|
||||||
|
d = now.day
|
||||||
|
|
||||||
|
if m >= WINTER_START_MONTH:
|
||||||
|
return True
|
||||||
|
if m <= 3:
|
||||||
|
return True
|
||||||
|
if m == WINTER_END_MONTH and d <= WINTER_END_DAY:
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def load_last_state():
|
|
||||||
"""Legge se c'era un allerta attiva"""
|
|
||||||
if not os.path.exists(STATE_FILE): return False
|
|
||||||
try:
|
|
||||||
with open(STATE_FILE, 'r') as f:
|
|
||||||
data = json.load(f)
|
|
||||||
return data.get("alert_active", False)
|
|
||||||
except: return False
|
|
||||||
|
|
||||||
def save_current_state(is_active):
|
def read_text_file(path: str) -> str:
|
||||||
"""Salva lo stato corrente"""
|
|
||||||
try:
|
try:
|
||||||
with open(STATE_FILE, 'w') as f:
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
json.dump({"alert_active": is_active, "updated": str(datetime.datetime.now())}, f)
|
return f.read().strip()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return ""
|
||||||
|
except PermissionError:
|
||||||
|
LOGGER.debug("Permission denied reading %s", path)
|
||||||
|
return ""
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Errore salvataggio stato: {e}")
|
LOGGER.exception("Error reading %s: %s", path, e)
|
||||||
|
return ""
|
||||||
|
|
||||||
def send_telegram_message(message):
|
|
||||||
if not TELEGRAM_BOT_TOKEN or "INSERISCI" in TELEGRAM_BOT_TOKEN:
|
|
||||||
print(f"[TEST OUT] {message}")
|
|
||||||
return
|
|
||||||
|
|
||||||
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
|
def load_bot_token() -> str:
|
||||||
|
tok = os.environ.get("TELEGRAM_BOT_TOKEN", "").strip()
|
||||||
|
if tok:
|
||||||
|
return tok
|
||||||
|
tok = read_text_file(TOKEN_FILE_HOME)
|
||||||
|
if tok:
|
||||||
|
return tok
|
||||||
|
tok = read_text_file(TOKEN_FILE_ETC)
|
||||||
|
return tok.strip() if tok else ""
|
||||||
|
|
||||||
|
|
||||||
|
def parse_time_to_local(t: str) -> datetime.datetime:
|
||||||
|
dt = parser.isoparse(t)
|
||||||
|
if dt.tzinfo is None:
|
||||||
|
return dt.replace(tzinfo=TZINFO)
|
||||||
|
return dt.astimezone(TZINFO)
|
||||||
|
|
||||||
|
|
||||||
|
def hhmm(dt: datetime.datetime) -> str:
|
||||||
|
return dt.strftime("%H:%M")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Telegram
|
||||||
|
# =============================================================================
|
||||||
|
def telegram_send_html(message_html: str) -> bool:
|
||||||
|
"""Invia solo in caso di allerta (mai per errori)."""
|
||||||
|
token = load_bot_token()
|
||||||
|
if not token:
|
||||||
|
LOGGER.warning("Telegram token missing: message not sent.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
url = f"https://api.telegram.org/bot{token}/sendMessage"
|
||||||
|
base_payload = {
|
||||||
|
"text": message_html,
|
||||||
|
"parse_mode": "HTML",
|
||||||
|
"disable_web_page_preview": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
sent_ok = False
|
||||||
|
with requests.Session() as s:
|
||||||
for chat_id in TELEGRAM_CHAT_IDS:
|
for chat_id in TELEGRAM_CHAT_IDS:
|
||||||
payload = {"chat_id": chat_id, "text": message, "parse_mode": "Markdown"}
|
payload = dict(base_payload)
|
||||||
|
payload["chat_id"] = chat_id
|
||||||
try:
|
try:
|
||||||
requests.post(url, json=payload, timeout=10)
|
resp = s.post(url, json=payload, timeout=15)
|
||||||
time.sleep(0.2)
|
if resp.status_code == 200:
|
||||||
|
sent_ok = True
|
||||||
|
else:
|
||||||
|
LOGGER.error("Telegram error chat_id=%s status=%s body=%s",
|
||||||
|
chat_id, resp.status_code, resp.text[:500])
|
||||||
|
time.sleep(0.25)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Errore invio a {chat_id}: {e}")
|
LOGGER.exception("Telegram exception chat_id=%s err=%s", chat_id, e)
|
||||||
|
|
||||||
def get_forecast(lat, lon):
|
return sent_ok
|
||||||
url = "https://api.open-meteo.com/v1/forecast"
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# State
|
||||||
|
# =============================================================================
|
||||||
|
def load_state() -> Dict:
|
||||||
|
default = {"alert_active": False, "signature": "", "updated": ""}
|
||||||
|
if os.path.exists(STATE_FILE):
|
||||||
|
try:
|
||||||
|
with open(STATE_FILE, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f) or {}
|
||||||
|
default.update(data)
|
||||||
|
except Exception as e:
|
||||||
|
LOGGER.exception("State read error: %s", e)
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def save_state(alert_active: bool, signature: str) -> None:
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
|
||||||
|
with open(STATE_FILE, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(
|
||||||
|
{"alert_active": alert_active, "signature": signature, "updated": now_local().isoformat()},
|
||||||
|
f,
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
LOGGER.exception("State write error: %s", e)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Open-Meteo
|
||||||
|
# =============================================================================
|
||||||
|
def get_forecast(session: requests.Session, lat: float, lon: float) -> Optional[Dict]:
|
||||||
params = {
|
params = {
|
||||||
"latitude": lat, "longitude": lon,
|
"latitude": lat,
|
||||||
|
"longitude": lon,
|
||||||
"hourly": "snowfall",
|
"hourly": "snowfall",
|
||||||
"models": "arome_france_hd",
|
"timezone": TZ,
|
||||||
"timezone": "Europe/Rome",
|
"forecast_days": 2,
|
||||||
"forecast_days": 2
|
"models": MODEL,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, params=params, timeout=5)
|
r = session.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=25)
|
||||||
response.raise_for_status()
|
if r.status_code == 400:
|
||||||
return response.json()
|
try:
|
||||||
except:
|
j = r.json()
|
||||||
|
LOGGER.error("Open-Meteo 400 (model=%s lat=%.4f lon=%.4f): %s", MODEL, lat, lon, j.get("reason", j))
|
||||||
|
except Exception:
|
||||||
|
LOGGER.error("Open-Meteo 400 (model=%s lat=%.4f lon=%.4f): %s", MODEL, lat, lon, r.text[:500])
|
||||||
|
return None
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
except Exception as e:
|
||||||
|
LOGGER.exception("Open-Meteo request error (model=%s lat=%.4f lon=%.4f): %s", MODEL, lat, lon, e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def calculate_sums(data):
|
|
||||||
if not data: return None
|
|
||||||
|
|
||||||
hourly = data.get("hourly", {})
|
# =============================================================================
|
||||||
times = hourly.get("time", [])
|
# Analytics
|
||||||
snow = hourly.get("snowfall", [])
|
# =============================================================================
|
||||||
|
def compute_snow_stats(data: Dict) -> Optional[Dict]:
|
||||||
|
hourly = data.get("hourly", {}) or {}
|
||||||
|
times = hourly.get("time", []) or []
|
||||||
|
snow = hourly.get("snowfall", []) or []
|
||||||
|
|
||||||
if not times or not snow: return None
|
n = min(len(times), len(snow))
|
||||||
|
if n == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
now = datetime.datetime.now(ZoneInfo("Europe/Rome"))
|
times = times[:n]
|
||||||
|
snow = snow[:n]
|
||||||
|
|
||||||
|
now = now_local()
|
||||||
start_idx = -1
|
start_idx = -1
|
||||||
for i, t_str in enumerate(times):
|
for i, t in enumerate(times):
|
||||||
try:
|
try:
|
||||||
t_obj = parser.isoparse(t_str).replace(tzinfo=ZoneInfo("Europe/Rome"))
|
if parse_time_to_local(t) >= now:
|
||||||
if t_obj >= now.replace(minute=0, second=0, microsecond=0):
|
|
||||||
start_idx = i
|
start_idx = i
|
||||||
break
|
break
|
||||||
except: continue
|
except Exception:
|
||||||
|
continue
|
||||||
|
if start_idx == -1:
|
||||||
|
return None
|
||||||
|
|
||||||
if start_idx == -1: return None
|
end_idx = min(start_idx + HOURS_AHEAD, n)
|
||||||
|
if end_idx <= start_idx:
|
||||||
|
return None
|
||||||
|
|
||||||
end = len(snow)
|
times_w = times[start_idx:end_idx]
|
||||||
# Calcola somme sugli orizzonti temporali
|
snow_w = [float(x) if x is not None else 0.0 for x in snow[start_idx:end_idx]]
|
||||||
def get_sum(hours):
|
dt_w = [parse_time_to_local(t) for t in times_w]
|
||||||
return sum(x for x in snow[start_idx:min(start_idx+hours, end)] if x)
|
|
||||||
|
# Accumuli informativi
|
||||||
|
def sum_h(h: int) -> float:
|
||||||
|
upto = min(h, len(snow_w))
|
||||||
|
return float(sum(snow_w[:upto]))
|
||||||
|
|
||||||
|
s3 = sum_h(3)
|
||||||
|
s6 = sum_h(6)
|
||||||
|
s12 = sum_h(12)
|
||||||
|
s24 = sum_h(24)
|
||||||
|
|
||||||
|
# Picco orario e prima occorrenza > soglia
|
||||||
|
peak = max(snow_w) if snow_w else 0.0
|
||||||
|
peak_time = ""
|
||||||
|
first_thr_time = ""
|
||||||
|
first_thr_val = 0.0
|
||||||
|
|
||||||
|
for i, v in enumerate(snow_w):
|
||||||
|
if v > peak:
|
||||||
|
peak = v
|
||||||
|
peak_time = hhmm(dt_w[i])
|
||||||
|
if (not first_thr_time) and (v > SNOW_HOURLY_THRESHOLD_CM):
|
||||||
|
first_thr_time = hhmm(dt_w[i])
|
||||||
|
first_thr_val = v
|
||||||
|
|
||||||
|
if not peak_time and peak > 0 and dt_w:
|
||||||
|
try:
|
||||||
|
peak_i = snow_w.index(peak)
|
||||||
|
peak_time = hhmm(dt_w[peak_i])
|
||||||
|
except Exception:
|
||||||
|
peak_time = ""
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"3h": get_sum(3),
|
"snow_3h": s3,
|
||||||
"6h": get_sum(6),
|
"snow_6h": s6,
|
||||||
"12h": get_sum(12),
|
"snow_12h": s12,
|
||||||
"24h": get_sum(24)
|
"snow_24h": s24,
|
||||||
|
"peak_hourly": float(peak),
|
||||||
|
"peak_time": peak_time,
|
||||||
|
"first_thr_time": first_thr_time,
|
||||||
|
"first_thr_val": float(first_thr_val),
|
||||||
|
"triggered": bool(first_thr_time),
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- LOGICA PRINCIPALE ---
|
|
||||||
|
|
||||||
def analyze_snow():
|
def point_summary(name: str, st: Dict) -> Dict:
|
||||||
# 1. Controllo Stagionale
|
return {
|
||||||
|
"name": name,
|
||||||
|
"triggered": bool(st["triggered"]),
|
||||||
|
"snow_3h": st["snow_3h"],
|
||||||
|
"snow_6h": st["snow_6h"],
|
||||||
|
"snow_12h": st["snow_12h"],
|
||||||
|
"snow_24h": st["snow_24h"],
|
||||||
|
"peak_hourly": st["peak_hourly"],
|
||||||
|
"peak_time": st["peak_time"],
|
||||||
|
"first_thr_time": st["first_thr_time"],
|
||||||
|
"first_thr_val": st["first_thr_val"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_signature(summaries: List[Dict]) -> str:
|
||||||
|
# Firma per evitare spam: arrotondiamo a 0.1 cm
|
||||||
|
parts = []
|
||||||
|
for s in summaries:
|
||||||
|
parts.append(
|
||||||
|
f"{s['name']}:t{int(s['triggered'])}"
|
||||||
|
f":24={s['snow_24h']:.1f}"
|
||||||
|
f":pk={s['peak_hourly']:.1f}"
|
||||||
|
f":ft={s['first_thr_time'] or '-'}"
|
||||||
|
)
|
||||||
|
return "|".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Main
|
||||||
|
# =============================================================================
|
||||||
|
def analyze_snow() -> None:
|
||||||
if not is_winter_season():
|
if not is_winter_season():
|
||||||
print("Stagione estiva. Script in pausa.")
|
LOGGER.info("Fuori stagione invernale: nessuna verifica/notify.")
|
||||||
|
save_state(False, "")
|
||||||
return
|
return
|
||||||
|
|
||||||
now_str = datetime.datetime.now(ZoneInfo("Europe/Rome")).strftime('%H:%M')
|
now_str = now_local().strftime("%H:%M")
|
||||||
print(f"--- Check Meteo {now_str} ---")
|
LOGGER.info("--- Check Neve AROME HD %s ---", now_str)
|
||||||
|
|
||||||
home_stats = None
|
state = load_state()
|
||||||
max_area_snow = 0.0
|
was_active = bool(state.get("alert_active", False))
|
||||||
area_details = ""
|
last_sig = state.get("signature", "")
|
||||||
|
|
||||||
# 2. Raccolta Dati
|
summaries: List[Dict] = []
|
||||||
|
|
||||||
|
with requests.Session() as session:
|
||||||
for p in POINTS:
|
for p in POINTS:
|
||||||
data = get_forecast(p["lat"], p["lon"])
|
data = get_forecast(session, p["lat"], p["lon"])
|
||||||
stats = calculate_sums(data)
|
if not data:
|
||||||
|
LOGGER.warning("Forecast non disponibile per %s (skip).", p["name"])
|
||||||
|
continue
|
||||||
|
|
||||||
if not stats: continue
|
st = compute_snow_stats(data)
|
||||||
|
if not st:
|
||||||
|
LOGGER.warning("Statistiche non calcolabili per %s (skip).", p["name"])
|
||||||
|
continue
|
||||||
|
|
||||||
if p["name"] == "🏠 Casa":
|
summaries.append(point_summary(p["name"], st))
|
||||||
home_stats = stats
|
time.sleep(0.2)
|
||||||
|
|
||||||
# Aggiorna il massimo rilevato in zona
|
if not summaries:
|
||||||
if stats["24h"] > max_area_snow:
|
LOGGER.error("Nessun punto ha restituito statistiche valide.")
|
||||||
max_area_snow = stats["24h"]
|
return
|
||||||
|
|
||||||
# Costruisci dettaglio se c'è neve
|
any_trigger = any(s["triggered"] for s in summaries)
|
||||||
if stats["24h"] > 0:
|
sig = build_signature(summaries)
|
||||||
area_details += f"{p['name']}: {stats['24h']:.1f}cm (12h: {stats['12h']:.1f})\n"
|
|
||||||
|
|
||||||
time.sleep(1)
|
# --- Scenario A: soglia superata ---
|
||||||
|
if any_trigger:
|
||||||
|
if (not was_active) or (sig != last_sig):
|
||||||
|
msg: List[str] = []
|
||||||
|
msg.append("❄️ <b>ALLERTA NEVE (AROME HD)</b>")
|
||||||
|
msg.append(f"🕒 <i>Aggiornamento ore {html.escape(now_str)}</i>")
|
||||||
|
msg.append(f"🛰️ <code>Modello: {html.escape(MODEL)}</code>")
|
||||||
|
msg.append(f"⏱️ <code>Finestra: prossime {HOURS_AHEAD} ore | Trigger: snowfall > {SNOW_HOURLY_THRESHOLD_CM:.1f} cm/h</code>")
|
||||||
|
msg.append("")
|
||||||
|
|
||||||
# 3. Decisione Alert
|
casa = next((s for s in summaries if s["name"] == "🏠 Casa"), None)
|
||||||
# C'è neve se a casa o nei dintorni l'accumulo è > soglia
|
if casa:
|
||||||
home_max = home_stats["24h"] if home_stats else 0.0
|
msg.append("🏠 <b>CASA</b>")
|
||||||
SNOW_DETECTED = (home_max > SOGLIA_NOTIFICA or max_area_snow > SOGLIA_NOTIFICA)
|
msg.append(f"• 03h: <b>{casa['snow_3h']:.1f}</b> cm | 06h: <b>{casa['snow_6h']:.1f}</b> cm")
|
||||||
|
msg.append(f"• 12h: <b>{casa['snow_12h']:.1f}</b> cm | 24h: <b>{casa['snow_24h']:.1f}</b> cm")
|
||||||
# Leggi stato precedente
|
if casa["triggered"]:
|
||||||
WAS_ACTIVE = load_last_state()
|
msg.append(
|
||||||
|
f"• Primo superamento soglia: <b>{html.escape(casa['first_thr_time'] or '—')}</b> "
|
||||||
# --- SCENARIO A: C'È NEVE (Nuova o Continua) ---
|
f"({casa['first_thr_val']:.1f} cm/h)"
|
||||||
if SNOW_DETECTED:
|
|
||||||
|
|
||||||
def f(v): return f"**{v:.1f}**" if v > 0 else f"{v:.1f}"
|
|
||||||
|
|
||||||
msg = f"❄️ **ALLERTA NEVE (AROME HD)**\n📅 _Aggiornamento ore {now_str}_\n\n"
|
|
||||||
|
|
||||||
if home_stats:
|
|
||||||
msg += f"🏠 **CASA:**\n"
|
|
||||||
msg += f"• 03h: {f(home_stats['3h'])} cm\n"
|
|
||||||
msg += f"• 06h: {f(home_stats['6h'])} cm\n"
|
|
||||||
msg += f"• 12h: {f(home_stats['12h'])} cm\n"
|
|
||||||
msg += f"• 24h: {f(home_stats['24h'])} cm\n\n"
|
|
||||||
|
|
||||||
if area_details:
|
|
||||||
msg += f"🌍 **NEL CIRCONDARIO (24h):**\n"
|
|
||||||
msg += f"`{area_details}`"
|
|
||||||
else:
|
|
||||||
msg += "🌍 Nessuna neve rilevante nei dintorni."
|
|
||||||
|
|
||||||
send_telegram_message(msg)
|
|
||||||
save_current_state(True) # Salva che l'allerta è attiva
|
|
||||||
print("Neve rilevata. Notifica inviata.")
|
|
||||||
|
|
||||||
# --- SCENARIO B: ALLARME RIENTRATO (Neve 0, ma prima c'era) ---
|
|
||||||
elif not SNOW_DETECTED and WAS_ACTIVE:
|
|
||||||
msg = (
|
|
||||||
f"🟢 **PREVISIONE NEVE ANNULLATA**\n"
|
|
||||||
f"📅 _Aggiornamento ore {now_str}_\n\n"
|
|
||||||
f"Le ultime previsioni AROME non indicano più accumuli nevosi rilevanti nelle prossime 24 ore.\n"
|
|
||||||
f"Situazione tornata alla normalità."
|
|
||||||
)
|
)
|
||||||
send_telegram_message(msg)
|
if casa["peak_hourly"] > 0:
|
||||||
save_current_state(False) # Resetta lo stato
|
msg.append(f"• Picco orario: <b>{casa['peak_hourly']:.1f}</b> cm/h (~{html.escape(casa['peak_time'] or '—')})")
|
||||||
print("Allarme rientrato. Notifica inviata.")
|
msg.append("")
|
||||||
|
|
||||||
# --- SCENARIO C: TUTTO TRANQUILLO (E lo era anche prima) ---
|
msg.append("🌍 <b>NEL CIRCONDARIO</b>")
|
||||||
|
lines = []
|
||||||
|
for s in summaries:
|
||||||
|
if not s["triggered"]:
|
||||||
|
continue
|
||||||
|
lines.append(
|
||||||
|
f"{s['name']}: primo > soglia alle {s['first_thr_time'] or '—'} "
|
||||||
|
f"({s['first_thr_val']:.1f} cm/h), picco {s['peak_hourly']:.1f} cm/h, 24h {s['snow_24h']:.1f} cm"
|
||||||
|
)
|
||||||
|
|
||||||
|
if lines:
|
||||||
|
msg.append("<pre>" + html.escape("\n".join(lines)) + "</pre>")
|
||||||
else:
|
else:
|
||||||
# Aggiorna timestamp ma mantieni false
|
msg.append("Nessun punto ha superato la soglia (anomalia).")
|
||||||
save_current_state(False)
|
|
||||||
print(f"Nessuna neve. Casa: {home_max}cm, Area: {max_area_snow}cm")
|
msg.append("<i>Fonte dati: Open-Meteo</i>")
|
||||||
|
|
||||||
|
ok = telegram_send_html("<br/>".join(msg))
|
||||||
|
if ok:
|
||||||
|
LOGGER.info("Notifica neve inviata.")
|
||||||
|
else:
|
||||||
|
LOGGER.warning("Notifica neve NON inviata (token mancante o errore Telegram).")
|
||||||
|
|
||||||
|
save_state(True, sig)
|
||||||
|
else:
|
||||||
|
LOGGER.info("Allerta già notificata e invariata.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# --- Scenario B: rientro (nessun superamento) ---
|
||||||
|
if was_active and not any_trigger:
|
||||||
|
msg = (
|
||||||
|
"🟢 <b>PREVISIONE NEVE ANNULLATA</b><br/>"
|
||||||
|
f"🕒 <i>Aggiornamento ore {html.escape(now_str)}</i><br/><br/>"
|
||||||
|
f"Nelle prossime {HOURS_AHEAD} ore non risultano ore con snowfall > {SNOW_HOURLY_THRESHOLD_CM:.1f} cm/h.<br/>"
|
||||||
|
"<i>Fonte dati: Open-Meteo</i>"
|
||||||
|
)
|
||||||
|
ok = telegram_send_html(msg)
|
||||||
|
if ok:
|
||||||
|
LOGGER.info("Rientro neve notificato.")
|
||||||
|
else:
|
||||||
|
LOGGER.warning("Rientro neve NON inviato (token mancante o errore Telegram).")
|
||||||
|
save_state(False, "")
|
||||||
|
return
|
||||||
|
|
||||||
|
# --- Scenario C: tranquillo ---
|
||||||
|
save_state(False, "")
|
||||||
|
top = sorted(summaries, key=lambda x: x["snow_24h"], reverse=True)[:3]
|
||||||
|
LOGGER.info(
|
||||||
|
"Nessuna neve sopra soglia. Top accumuli 24h: %s",
|
||||||
|
" | ".join(f"{t['name']}={t['snow_24h']:.1f}cm (pk {t['peak_hourly']:.1f}cm/h)" for t in top)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
analyze_snow()
|
analyze_snow()
|
||||||
|
|||||||
Reference in New Issue
Block a user