567 lines
21 KiB
Python
567 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import argparse
|
|
import datetime
|
|
import html
|
|
import json
|
|
import logging
|
|
import os
|
|
import time
|
|
from logging.handlers import RotatingFileHandler
|
|
from typing import Dict, List, Optional, Tuple
|
|
from zoneinfo import ZoneInfo
|
|
|
|
import requests
|
|
from dateutil import parser
|
|
|
|
# =============================================================================
|
|
# FREEZE ALERT (next 48h) - San Marino (Casa)
|
|
#
|
|
# Scopo:
|
|
# Notificare su Telegram se nelle prossime 48 ore è prevista una temperatura
|
|
# minima < SOGLIA_GELO.
|
|
#
|
|
# Requisiti operativi applicati (come gli altri script):
|
|
# - Nessun token in chiaro (legge da env o file /etc/telegram_dpc_bot_token)
|
|
# - Log su file + modalità DEBUG (DEBUG=1)
|
|
# - Timezone robusta (naive -> Europe/Rome; offset -> conversione)
|
|
# - Nessun Telegram in caso di errori (solo log)
|
|
# - Anti-spam: notifica solo su nuovo evento o variazione significativa
|
|
#
|
|
# Esecuzione:
|
|
# DEBUG=1 python3 freeze_alert.py
|
|
# tail -n 200 freeze_alert.log
|
|
# =============================================================================
|
|
|
|
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"
|
|
|
|
# ----------------- LOCATION -----------------
|
|
LAT = 43.9356
|
|
LON = 12.4296
|
|
LOCATION_NAME = "🏠 Casa (Strada Cà Toro)"
|
|
|
|
# ----------------- THRESHOLD -----------------
|
|
SOGLIA_GELO = 0.0 # °C (allerta se min <= 0.0°C, include anche temperature esattamente a zero)
|
|
|
|
# ----------------- HORIZON -----------------
|
|
HOURS_AHEAD = 48
|
|
FORECAST_DAYS = 3 # per coprire bene 48h
|
|
|
|
# ----------------- TIMEZONE -----------------
|
|
TZ = "Europe/Berlin"
|
|
TZINFO = ZoneInfo(TZ)
|
|
|
|
# ----------------- FILES -----------------
|
|
STATE_FILE = "/home/daniely/docker/telegram-bot/freeze_state.json"
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
LOG_FILE = os.path.join(BASE_DIR, "freeze_alert.log")
|
|
|
|
# ----------------- OPEN-METEO -----------------
|
|
OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"
|
|
HTTP_HEADERS = {"User-Agent": "rpi-freeze-alert/2.0"}
|
|
|
|
|
|
# =============================================================================
|
|
# LOGGING
|
|
# =============================================================================
|
|
def setup_logger() -> logging.Logger:
|
|
logger = logging.getLogger("freeze_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()
|
|
|
|
|
|
# =============================================================================
|
|
# UTILS
|
|
# =============================================================================
|
|
def now_local() -> datetime.datetime:
|
|
return datetime.datetime.now(TZINFO)
|
|
|
|
|
|
def read_text_file(path: str) -> str:
|
|
try:
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
return f.read().strip()
|
|
except FileNotFoundError:
|
|
return ""
|
|
except PermissionError:
|
|
LOGGER.debug("Permission denied reading %s", path)
|
|
return ""
|
|
except Exception as e:
|
|
LOGGER.exception("Error reading %s: %s", path, e)
|
|
return ""
|
|
|
|
|
|
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:
|
|
"""
|
|
Parsing robusto:
|
|
- se naive (frequente con timezone=Europe/Rome) -> interpreta come Europe/Rome
|
|
- se con offset -> converte in Europe/Rome
|
|
"""
|
|
dt = parser.isoparse(t)
|
|
if dt.tzinfo is None:
|
|
return dt.replace(tzinfo=TZINFO)
|
|
return dt.astimezone(TZINFO)
|
|
|
|
|
|
def fmt_dt(dt: datetime.datetime) -> str:
|
|
return dt.strftime("%d/%m alle %H:%M")
|
|
|
|
|
|
# =============================================================================
|
|
# TELEGRAM
|
|
# =============================================================================
|
|
def telegram_send_html(message_html: str, chat_ids: Optional[List[str]] = None) -> bool:
|
|
"""
|
|
Non solleva eccezioni. Ritorna True se almeno un invio ha successo.
|
|
IMPORTANTE: chiamare solo per allerte (mai per errori).
|
|
|
|
Args:
|
|
message_html: Messaggio HTML da inviare
|
|
chat_ids: Lista di chat IDs (default: TELEGRAM_CHAT_IDS)
|
|
"""
|
|
token = load_bot_token()
|
|
if not token:
|
|
LOGGER.warning("Telegram token missing: message not sent.")
|
|
return False
|
|
|
|
if chat_ids is None:
|
|
chat_ids = TELEGRAM_CHAT_IDS
|
|
|
|
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 chat_ids:
|
|
payload = dict(base_payload)
|
|
payload["chat_id"] = chat_id
|
|
try:
|
|
resp = s.post(url, json=payload, timeout=15)
|
|
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:
|
|
LOGGER.exception("Telegram exception chat_id=%s err=%s", chat_id, e)
|
|
|
|
return sent_ok
|
|
|
|
|
|
# =============================================================================
|
|
# STATE
|
|
# =============================================================================
|
|
def load_state() -> Dict:
|
|
default = {
|
|
"alert_active": False,
|
|
"min_temp": 100.0,
|
|
"min_time": "",
|
|
"signature": "",
|
|
"updated": "",
|
|
"notified_periods": [], # Lista di fasce orarie già notificate: [{"start": iso, "end": iso}, ...]
|
|
}
|
|
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)
|
|
# Assicura che notified_periods esista
|
|
if "notified_periods" not in default:
|
|
default["notified_periods"] = []
|
|
except Exception as e:
|
|
LOGGER.exception("State read error: %s", e)
|
|
return default
|
|
|
|
|
|
def save_state(state: Dict) -> None:
|
|
try:
|
|
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
|
|
state["updated"] = now_local().isoformat()
|
|
with open(STATE_FILE, "w", encoding="utf-8") as f:
|
|
json.dump(state, f, ensure_ascii=False, indent=2)
|
|
except Exception as e:
|
|
LOGGER.exception("State write error: %s", e)
|
|
|
|
|
|
# =============================================================================
|
|
# OPEN-METEO
|
|
# =============================================================================
|
|
def get_forecast() -> Optional[Dict]:
|
|
params = {
|
|
"latitude": LAT,
|
|
"longitude": LON,
|
|
"hourly": "temperature_2m",
|
|
"timezone": TZ,
|
|
"forecast_days": FORECAST_DAYS,
|
|
"models": "meteofrance_seamless", # Usa seamless per avere minutely_15
|
|
"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)
|
|
if r.status_code == 400:
|
|
try:
|
|
j = r.json()
|
|
LOGGER.error("Open-Meteo 400: %s", j.get("reason", j))
|
|
except Exception:
|
|
LOGGER.error("Open-Meteo 400: %s", r.text[:500])
|
|
return None
|
|
r.raise_for_status()
|
|
return r.json()
|
|
except Exception as e:
|
|
LOGGER.exception("Open-Meteo request error: %s", e)
|
|
return None
|
|
|
|
|
|
def compute_freezing_periods(data: Dict) -> Tuple[Optional[float], Optional[datetime.datetime], List[Tuple[datetime.datetime, datetime.datetime]]]:
|
|
"""
|
|
Calcola la temperatura minima e tutte le fasce orarie con gelo (temp <= 0°C).
|
|
|
|
Returns:
|
|
(min_temp_val, min_temp_time, freezing_periods)
|
|
freezing_periods: lista di tuple (start_time, end_time) per ogni fascia oraria con gelo
|
|
"""
|
|
hourly = data.get("hourly", {}) or {}
|
|
minutely = data.get("minutely_15", {}) or {}
|
|
times = hourly.get("time", []) or []
|
|
temps = hourly.get("temperature_2m", []) or []
|
|
|
|
LOGGER.debug("Dati hourly: %d timestamps, %d temperature", len(times), len(temps))
|
|
|
|
# Usa minutely_15 se disponibile per maggiore precisione
|
|
minutely_times = minutely.get("time", []) or []
|
|
minutely_temps = minutely.get("temperature_2m", []) or []
|
|
use_minutely = bool(minutely_times) and len(minutely_times) > 0
|
|
|
|
LOGGER.debug("Dati minutely_15: %d timestamps, %d temperature, use_minutely=%s",
|
|
len(minutely_times), len(minutely_temps), use_minutely)
|
|
|
|
now = now_local()
|
|
limit_time = now + datetime.timedelta(hours=HOURS_AHEAD)
|
|
LOGGER.debug("Finestra temporale: da %s a %s", now.isoformat(), limit_time.isoformat())
|
|
|
|
min_temp_val = 100.0
|
|
min_temp_time: Optional[datetime.datetime] = None
|
|
freezing_periods: List[Tuple[datetime.datetime, datetime.datetime]] = []
|
|
temps_near_zero = [] # Per debug: temperature vicine allo zero (0-2°C)
|
|
|
|
# Priorità a minutely_15 se disponibile (risoluzione 15 minuti)
|
|
if use_minutely:
|
|
for i, t_str in enumerate(minutely_times):
|
|
try:
|
|
t_obj = parse_time_to_local(t_str)
|
|
except Exception:
|
|
continue
|
|
|
|
if t_obj <= now or t_obj > limit_time:
|
|
continue
|
|
|
|
try:
|
|
temp = float(minutely_temps[i]) if i < len(minutely_temps) and minutely_temps[i] is not None else 100.0
|
|
except Exception:
|
|
continue
|
|
|
|
# Raccogli temperature vicine allo zero per debug
|
|
if 0.0 <= temp <= 2.0:
|
|
temps_near_zero.append((temp, t_obj))
|
|
|
|
if temp < min_temp_val:
|
|
min_temp_val = temp
|
|
min_temp_time = t_obj
|
|
else:
|
|
# Fallback a hourly
|
|
n = min(len(times), len(temps))
|
|
if n == 0:
|
|
return None, None, []
|
|
|
|
for i in range(n):
|
|
try:
|
|
t_obj = parse_time_to_local(times[i])
|
|
except Exception:
|
|
continue
|
|
|
|
if t_obj <= now or t_obj > limit_time:
|
|
continue
|
|
|
|
try:
|
|
temp = float(temps[i])
|
|
except Exception:
|
|
continue
|
|
|
|
# Raccogli temperature vicine allo zero per debug
|
|
if 0.0 <= temp <= 2.0:
|
|
temps_near_zero.append((temp, t_obj))
|
|
|
|
if temp < min_temp_val:
|
|
min_temp_val = temp
|
|
min_temp_time = t_obj
|
|
|
|
# Raggruppa le temperature <= 0°C in fasce orarie continue
|
|
# Una fascia oraria è un periodo continuo di tempo con temperatura <= 0°C
|
|
freezing_times: List[datetime.datetime] = []
|
|
if use_minutely:
|
|
for i, t_str in enumerate(minutely_times):
|
|
try:
|
|
t_obj = parse_time_to_local(t_str)
|
|
except Exception:
|
|
continue
|
|
if t_obj <= now or t_obj > limit_time:
|
|
continue
|
|
try:
|
|
temp = float(minutely_temps[i]) if i < len(minutely_temps) and minutely_temps[i] is not None else 100.0
|
|
except Exception:
|
|
continue
|
|
if temp <= SOGLIA_GELO:
|
|
freezing_times.append(t_obj)
|
|
else:
|
|
for i in range(min(len(times), len(temps))):
|
|
try:
|
|
t_obj = parse_time_to_local(times[i])
|
|
except Exception:
|
|
continue
|
|
if t_obj <= now or t_obj > limit_time:
|
|
continue
|
|
try:
|
|
temp = float(temps[i])
|
|
except Exception:
|
|
continue
|
|
if temp <= SOGLIA_GELO:
|
|
freezing_times.append(t_obj)
|
|
|
|
# Raggruppa in fasce orarie continue (max gap di 1 ora tra due timestamp consecutivi)
|
|
if freezing_times:
|
|
freezing_times.sort()
|
|
current_start = freezing_times[0]
|
|
current_end = freezing_times[0]
|
|
|
|
for t in freezing_times[1:]:
|
|
# Se il gap è > 1 ora, chiudi la fascia corrente e inizia una nuova
|
|
if (t - current_end).total_seconds() > 3600:
|
|
freezing_periods.append((current_start, current_end))
|
|
current_start = t
|
|
current_end = t
|
|
# Aggiungi l'ultima fascia
|
|
freezing_periods.append((current_start, current_end))
|
|
|
|
if min_temp_time is None:
|
|
LOGGER.warning("Nessuna temperatura minima trovata nella finestra temporale")
|
|
return None, None, []
|
|
|
|
LOGGER.debug("Temperatura minima trovata: %.1f°C alle %s", min_temp_val, min_temp_time.isoformat())
|
|
LOGGER.info("Fasce orarie con gelo rilevate: %d", len(freezing_periods))
|
|
for i, (start, end) in enumerate(freezing_periods[:5]): # Mostra prime 5
|
|
LOGGER.info(" Fascia %d: %s - %s", i+1, start.strftime("%d/%m %H:%M"), end.strftime("%d/%m %H:%M"))
|
|
|
|
# Log temperature vicine allo zero per debug
|
|
if temps_near_zero:
|
|
temps_near_zero.sort(key=lambda x: x[0]) # Ordina per temperatura
|
|
LOGGER.info("Temperature vicine allo zero (0-2°C) rilevate: %d occorrenze", len(temps_near_zero))
|
|
for temp, t_obj in temps_near_zero[:5]: # Mostra prime 5
|
|
LOGGER.info(" %.1f°C alle %s", temp, t_obj.strftime("%d/%m %H:%M"))
|
|
|
|
return float(min_temp_val), min_temp_time, freezing_periods
|
|
|
|
|
|
# =============================================================================
|
|
# MAIN
|
|
# =============================================================================
|
|
def analyze_freeze(chat_ids: Optional[List[str]] = None, debug_mode: bool = False) -> None:
|
|
LOGGER.info("--- Controllo Gelo (next %sh) ---", HOURS_AHEAD)
|
|
|
|
data = get_forecast()
|
|
if not data:
|
|
# errori: solo log
|
|
return
|
|
|
|
result = compute_freezing_periods(data)
|
|
if result[0] is None:
|
|
LOGGER.error("Impossibile calcolare minima nelle prossime %s ore.", HOURS_AHEAD)
|
|
return
|
|
|
|
min_temp_val, min_temp_time, freezing_periods = result
|
|
LOGGER.info("Temperatura minima rilevata: %.1f°C alle %s", min_temp_val, min_temp_time.isoformat())
|
|
LOGGER.info("Soglia gelo: %.1f°C", SOGLIA_GELO)
|
|
|
|
# Segnala se temperatura <= soglia (include anche 0.0°C e temperature vicine allo zero)
|
|
# Cambiato da < a <= per includere anche temperature esattamente a 0.0°C
|
|
is_freezing = (min_temp_val <= SOGLIA_GELO)
|
|
LOGGER.info("Condizione gelo: min_temp_val (%.1f) <= SOGLIA_GELO (%.1f) = %s",
|
|
min_temp_val, SOGLIA_GELO, is_freezing)
|
|
|
|
state = load_state()
|
|
was_active = bool(state.get("alert_active", False))
|
|
notified_periods = state.get("notified_periods", [])
|
|
LOGGER.info("Stato precedente: alert_active=%s, last_min_temp=%.1f, notified_periods=%d",
|
|
was_active, state.get("min_temp", 100.0), len(notified_periods))
|
|
|
|
# Verifica se ci sono nuove fasce orarie con gelo non ancora notificate
|
|
new_periods = []
|
|
for period_start, period_end in freezing_periods:
|
|
is_new = True
|
|
for notified in notified_periods:
|
|
# Una fascia è considerata "già notificata" se si sovrappone significativamente
|
|
# (almeno 1 ora di sovrapposizione) con una fascia già notificata
|
|
try:
|
|
notif_start = parser.isoparse(notified["start"])
|
|
notif_end = parser.isoparse(notified["end"])
|
|
# Calcola sovrapposizione
|
|
overlap_start = max(period_start, notif_start)
|
|
overlap_end = min(period_end, notif_end)
|
|
if overlap_start < overlap_end:
|
|
overlap_hours = (overlap_end - overlap_start).total_seconds() / 3600
|
|
if overlap_hours >= 1.0: # Almeno 1 ora di sovrapposizione
|
|
is_new = False
|
|
break
|
|
except Exception:
|
|
continue
|
|
if is_new:
|
|
new_periods.append((period_start, period_end))
|
|
|
|
# invia se:
|
|
# - prima non era attivo, oppure
|
|
# - peggiora di almeno 2°C rispetto alla minima precedente, oppure
|
|
# - c'è almeno una nuova fascia oraria con gelo non ancora notificata
|
|
prev_min = float(state.get("min_temp", 100.0) or 100.0)
|
|
has_new_periods = len(new_periods) > 0
|
|
significant_worsening = (min_temp_val < prev_min - 2.0)
|
|
|
|
should_notify = (not was_active) or significant_worsening or has_new_periods
|
|
|
|
# In modalità debug, bypassa tutti i controlli anti-spam e invia sempre
|
|
if debug_mode:
|
|
should_notify = True
|
|
LOGGER.info("[DEBUG MODE] Bypass anti-spam: invio forzato")
|
|
|
|
LOGGER.info("Nuove fasce orarie con gelo: %d (notificate: %d)", len(new_periods), len(notified_periods))
|
|
if has_new_periods:
|
|
for i, (start, end) in enumerate(new_periods):
|
|
LOGGER.info(" Nuova fascia %d: %s - %s", i+1, start.strftime("%d/%m %H:%M"), end.strftime("%d/%m %H:%M"))
|
|
|
|
if should_notify:
|
|
# Costruisci messaggio con dettagli sulle nuove fasce orarie
|
|
period_details = []
|
|
if has_new_periods:
|
|
for start, end in new_periods[:3]: # Max 3 fasce nel messaggio
|
|
if start.date() == end.date():
|
|
period_details.append(f"{start.strftime('%d/%m %H:%M')}-{end.strftime('%H:%M')}")
|
|
else:
|
|
period_details.append(f"{start.strftime('%d/%m %H:%M')}-{end.strftime('%d/%m %H:%M')}")
|
|
|
|
msg_parts = [
|
|
"❄️ <b>ALLERTA GELO</b>\n",
|
|
f"📍 {html.escape(LOCATION_NAME)}\n\n",
|
|
f"Minima prevista (entro {HOURS_AHEAD}h): <b>{min_temp_val:.1f}°C</b>\n",
|
|
f"📅 Quando: <b>{html.escape(fmt_dt(min_temp_time))}</b>",
|
|
]
|
|
if period_details:
|
|
msg_parts.append("\n🕒 Fasce orarie: " + ", ".join(period_details))
|
|
msg_parts.append("\n\n<i>Proteggere piante e tubature esterne.</i>")
|
|
|
|
msg = "".join(msg_parts)
|
|
ok = telegram_send_html(msg, chat_ids=chat_ids)
|
|
if ok:
|
|
LOGGER.info("Allerta gelo inviata. Tmin=%.1f°C at %s, nuove fasce: %d",
|
|
min_temp_val, min_temp_time.isoformat(), len(new_periods))
|
|
# Aggiorna le fasce notificate
|
|
for start, end in new_periods:
|
|
notified_periods.append({
|
|
"start": start.isoformat(),
|
|
"end": end.isoformat(),
|
|
})
|
|
else:
|
|
LOGGER.warning("Allerta gelo NON inviata (token mancante o errore Telegram).")
|
|
|
|
else:
|
|
LOGGER.info("Gelo già notificato (nessuna nuova fascia oraria, peggioramento < 2°C). Tmin=%.1f°C", min_temp_val)
|
|
|
|
state.update({
|
|
"alert_active": True,
|
|
"min_temp": min_temp_val,
|
|
"min_time": min_temp_time.isoformat(),
|
|
"notified_periods": notified_periods,
|
|
})
|
|
save_state(state)
|
|
return
|
|
|
|
# --- RIENTRO ---
|
|
if was_active and not is_freezing:
|
|
msg = (
|
|
"☀️ <b>RISCHIO GELO RIENTRATO</b>\n"
|
|
f"📍 {html.escape(LOCATION_NAME)}\n\n"
|
|
f"Le previsioni nelle prossime {HOURS_AHEAD} ore indicano temperature sopra lo zero.\n"
|
|
f"Minima prevista: <b>{min_temp_val:.1f}°C</b> (alle {html.escape(fmt_dt(min_temp_time))})."
|
|
)
|
|
ok = telegram_send_html(msg, chat_ids=chat_ids)
|
|
if ok:
|
|
LOGGER.info("Rientro gelo notificato. Tmin=%.1f°C", min_temp_val)
|
|
else:
|
|
LOGGER.warning("Rientro gelo NON inviato (token mancante o errore Telegram).")
|
|
|
|
state.update({
|
|
"alert_active": False,
|
|
"min_temp": min_temp_val,
|
|
"min_time": min_temp_time.isoformat(),
|
|
"notified_periods": [], # Reset quando il gelo rientra
|
|
})
|
|
save_state(state)
|
|
return
|
|
|
|
# --- TRANQUILLO ---
|
|
state.update({
|
|
"alert_active": False,
|
|
"min_temp": min_temp_val,
|
|
"min_time": min_temp_time.isoformat(),
|
|
"notified_periods": [], # Reset quando non c'è gelo
|
|
})
|
|
save_state(state)
|
|
LOGGER.info("Nessun gelo. Tmin=%.1f°C at %s", min_temp_val, min_temp_time.isoformat())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
arg_parser = argparse.ArgumentParser(description="Freeze alert")
|
|
arg_parser.add_argument("--debug", action="store_true", help="Invia messaggi solo all'admin (chat ID: %s)" % TELEGRAM_CHAT_IDS[0])
|
|
args = arg_parser.parse_args()
|
|
|
|
# In modalità debug, invia solo al primo chat ID (admin) e bypassa anti-spam
|
|
chat_ids = [TELEGRAM_CHAT_IDS[0]] if args.debug else None
|
|
|
|
analyze_freeze(chat_ids=chat_ids, debug_mode=args.debug)
|