Backup automatico script del 2026-01-04 07:00
This commit is contained in:
BIN
scripts/pi2-backup/._super_watchdog.sh
Executable file
BIN
scripts/pi2-backup/._super_watchdog.sh
Executable file
Binary file not shown.
BIN
services/telegram-bot/._bot.py
Executable file
BIN
services/telegram-bot/._bot.py
Executable file
Binary file not shown.
BIN
services/telegram-bot/._cam.py
Executable file
BIN
services/telegram-bot/._cam.py
Executable file
Binary file not shown.
BIN
services/telegram-bot/._meteo.py
Executable file
BIN
services/telegram-bot/._meteo.py
Executable file
Binary file not shown.
414
services/telegram-bot/bot.py
Normal file → Executable file
414
services/telegram-bot/bot.py
Normal file → Executable file
@@ -5,9 +5,6 @@ import datetime
|
|||||||
import requests
|
import requests
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
from dateutil import parser
|
|
||||||
from typing import Dict, List, Optional, Tuple
|
|
||||||
|
|
||||||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
|
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
|
||||||
from telegram.ext import (
|
from telegram.ext import (
|
||||||
Application,
|
Application,
|
||||||
@@ -18,34 +15,28 @@ from telegram.ext import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# LOOGLE BOT V7.9 (ULTIMATE + GLOBAL GFS FIX)
|
# LOOGLE BOT V9.0 (ULTIMATE + CAMERAS + MODULAR)
|
||||||
# - Dashboard Sistema
|
# - Dashboard Sistema (SSH/WOL/Monitor)
|
||||||
# - Meteo Smart: Arome (EU), Icon (EU-Est), JMA (JP), GFS (Mondo)
|
# - Meteo Smart (Meteo.py / Previsione7.py)
|
||||||
# - Multi-User Security
|
# - CCTV Hub (Cam.py + FFMPEG)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# --- CONFIGURAZIONE ---
|
# --- CONFIGURAZIONE AMBIENTE ---
|
||||||
BOT_TOKEN = os.environ.get('BOT_TOKEN')
|
BOT_TOKEN = os.environ.get('BOT_TOKEN')
|
||||||
|
|
||||||
# Gestione Multi-Utente
|
|
||||||
allowed_users_raw = os.environ.get('ALLOWED_USER_ID', '')
|
allowed_users_raw = os.environ.get('ALLOWED_USER_ID', '')
|
||||||
ALLOWED_IDS = [int(x.strip()) for x in allowed_users_raw.split(',') if x.strip().isdigit()]
|
ALLOWED_IDS = [int(x.strip()) for x in allowed_users_raw.split(',') if x.strip().isdigit()]
|
||||||
|
|
||||||
# Configurazione Sistema
|
|
||||||
SSH_USER = "daniely"
|
SSH_USER = "daniely"
|
||||||
NAS_USER = "daniely"
|
NAS_USER = "daniely"
|
||||||
MASTER_IP = "192.168.128.80"
|
MASTER_IP = "192.168.128.80"
|
||||||
|
|
||||||
# Configurazione Meteo
|
|
||||||
HOME_LAT = 43.9356
|
|
||||||
HOME_LON = 12.4296
|
|
||||||
HOME_NAME = "🏠 Casa (Strada Cà Toro)"
|
|
||||||
TZ = "Europe/Rome"
|
TZ = "Europe/Rome"
|
||||||
TZINFO = ZoneInfo(TZ)
|
TZINFO = ZoneInfo(TZ)
|
||||||
|
|
||||||
OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"
|
# --- GESTIONE PERCORSI DINAMICA (DOCKER FRIENDLY) ---
|
||||||
GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search"
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
HTTP_HEADERS = {"User-Agent": "loogle-bot-v7.9"}
|
METEO_SCRIPT = os.path.join(SCRIPT_DIR, "meteo.py")
|
||||||
|
METEO7_SCRIPT = os.path.join(SCRIPT_DIR, "previsione7.py")
|
||||||
|
CAM_SCRIPT = os.path.join(SCRIPT_DIR, "cam.py")
|
||||||
|
|
||||||
# --- LISTE DISPOSITIVI ---
|
# --- LISTE DISPOSITIVI ---
|
||||||
CORE_DEVICES = [
|
CORE_DEVICES = [
|
||||||
@@ -67,17 +58,19 @@ INFRA_DEVICES = [
|
|||||||
{"name": "🔌 Sw Tav", "ip": "192.168.128.108"}
|
{"name": "🔌 Sw Tav", "ip": "192.168.128.108"}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Configurazione Logging
|
||||||
logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO)
|
logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# SEZIONE 1: FUNZIONI SISTEMA (SSH, PING, UTILS)
|
# SEZIONE 1: FUNZIONI UTILI E HELPER
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
def run_cmd(command, ip=None, user=None):
|
def run_cmd(command, ip=None, user=None):
|
||||||
|
"""Esegue comandi shell locali o via SSH"""
|
||||||
try:
|
try:
|
||||||
if ip == "127.0.0.1" or ip is None:
|
if ip == "127.0.0.1" or ip is None:
|
||||||
return subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, timeout=5).decode('utf-8').strip()
|
return subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, timeout=8).decode('utf-8').strip()
|
||||||
else:
|
else:
|
||||||
safe_cmd = command.replace("'", "'\\''")
|
safe_cmd = command.replace("'", "'\\''")
|
||||||
full_cmd = f"ssh -o LogLevel=ERROR -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 {user}@{ip} '{safe_cmd}'"
|
full_cmd = f"ssh -o LogLevel=ERROR -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 {user}@{ip} '{safe_cmd}'"
|
||||||
@@ -118,225 +111,27 @@ def read_log_file(filepath, lines=15):
|
|||||||
except Exception as e: return f"Errore: {str(e)}"
|
except Exception as e: return f"Errore: {str(e)}"
|
||||||
|
|
||||||
def run_speedtest():
|
def run_speedtest():
|
||||||
try: return subprocess.check_output("speedtest-cli --simple", shell=True, timeout=50).decode('utf-8')
|
try: return subprocess.check_output("speedtest-cli --simple", shell=True, timeout=60).decode('utf-8')
|
||||||
except: return "Errore Speedtest"
|
except: return "Errore Speedtest"
|
||||||
|
|
||||||
# =============================================================================
|
def call_script_text(script_path, args_list):
|
||||||
# SEZIONE 2: METEO INTELLIGENTE (MULTI-MODELLO MOSAICO)
|
"""Wrapper per lanciare script che restituiscono testo (Meteo)"""
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
def now_local() -> datetime.datetime:
|
|
||||||
return datetime.datetime.now(TZINFO)
|
|
||||||
|
|
||||||
def parse_time(t: str) -> datetime.datetime:
|
|
||||||
dt = parser.isoparse(t)
|
|
||||||
if dt.tzinfo is None: return dt.replace(tzinfo=TZINFO)
|
|
||||||
return dt.astimezone(TZINFO)
|
|
||||||
|
|
||||||
def degrees_to_cardinal(d: int) -> str:
|
|
||||||
dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
|
|
||||||
return dirs[round(d / 45) % 8]
|
|
||||||
|
|
||||||
def get_icon_set(prec, snow, code, is_day, cloud, vis, temp, rain, gust, cape):
|
|
||||||
sky = "☁️"
|
|
||||||
if code in (95, 96, 99): sky = "⛈️" if prec > 0 else "🌩️"
|
|
||||||
elif (code in (45, 48) or vis < 1000) and prec < 1: sky = "🌫️"
|
|
||||||
elif prec >= 0.1: sky = "🌨️" if snow > 0 else "🌧️"
|
|
||||||
elif cloud <= 20: sky = "☀️" if is_day else "🌙"
|
|
||||||
elif cloud <= 40: sky = "🌤️" if is_day else "🌙"
|
|
||||||
elif cloud <= 60: sky = "⛅️"
|
|
||||||
elif cloud <= 80: sky = "🌥️"
|
|
||||||
|
|
||||||
sgx = "-"
|
|
||||||
if snow > 0 or (code in (71,73,75,77,85,86) if code else False): sgx = "☃️"
|
|
||||||
elif temp < 0 or (code in (66,67) if code else False): sgx = "🧊"
|
|
||||||
elif cape > 2000: sgx = "🌪️"
|
|
||||||
elif cape > 1000: sgx = "⚡"
|
|
||||||
elif temp > 35: sgx = "🥵"
|
|
||||||
elif rain > 4: sgx = "☔️"
|
|
||||||
elif gust > 50: sgx = "💨"
|
|
||||||
return sky, sgx
|
|
||||||
|
|
||||||
def get_coordinates(city_name: str):
|
|
||||||
"""
|
|
||||||
Cerca le coordinate della città con fallback EN.
|
|
||||||
"""
|
|
||||||
# 1. Tentativo ITALIANO
|
|
||||||
params = {"name": city_name, "count": 10, "language": "it", "format": "json"}
|
|
||||||
try:
|
try:
|
||||||
r = requests.get(GEOCODING_URL, params=params, headers=HTTP_HEADERS, timeout=10)
|
cmd = ["python3", script_path] + args_list
|
||||||
data = r.json()
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||||
if "results" in data and len(data["results"]) > 0:
|
return result.stdout.strip() if result.returncode == 0 else f"⚠️ Errore Script:\n{result.stderr}"
|
||||||
res = data["results"][0]
|
except Exception as e: return f"❌ Errore esecuzione: {e}"
|
||||||
cc = res.get("country_code", "IT").upper()
|
|
||||||
name = f"{res.get('name')} ({cc})"
|
|
||||||
return res["latitude"], res["longitude"], name, cc
|
|
||||||
except Exception as e: logger.error(f"Geocoding IT error: {e}")
|
|
||||||
|
|
||||||
# 2. Tentativo FALLBACK INGLESE
|
|
||||||
try:
|
|
||||||
params["language"] = "en"
|
|
||||||
r = requests.get(GEOCODING_URL, params=params, headers=HTTP_HEADERS, timeout=10)
|
|
||||||
data = r.json()
|
|
||||||
if "results" in data and len(data["results"]) > 0:
|
|
||||||
res = data["results"][0]
|
|
||||||
cc = res.get("country_code", "IT").upper()
|
|
||||||
name = f"{res.get('name')} ({cc})"
|
|
||||||
return res["latitude"], res["longitude"], name, cc
|
|
||||||
except Exception as e: logger.error(f"Geocoding EN error: {e}")
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def choose_best_model(lat, lon, cc):
|
|
||||||
"""
|
|
||||||
Seleziona il modello migliore.
|
|
||||||
Fallback Generale: NOAA GFS (perché ha sempre Visibilità/Cape).
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 1. GIAPPONE -> JMA MSM
|
|
||||||
if cc == 'JP':
|
|
||||||
return "jma_msm", "JMA MSM (5km)"
|
|
||||||
|
|
||||||
# 2. SCANDINAVIA -> Yr.no
|
|
||||||
if cc in ['NO', 'SE', 'FI', 'DK', 'IS']:
|
|
||||||
return "metno_nordic", "Yr.no (Nordic)"
|
|
||||||
|
|
||||||
# 3. UK & IRLANDA -> UK Met Office
|
|
||||||
if cc in ['GB', 'IE']:
|
|
||||||
return "ukmo_global", "UK MetOffice"
|
|
||||||
|
|
||||||
# 4. TUNING ITALIA (Mosaico)
|
|
||||||
if cc == 'IT' or cc == 'SM':
|
|
||||||
# ZONA 1: Nord-Ovest, Tirreno, Sardegna (Lon <= 13.0, Lat > 40.5)
|
|
||||||
if lon <= 13.0 and lat > 40.5:
|
|
||||||
return "meteofrance_arome_france_hd", "Arome HD"
|
|
||||||
# ZONA 2: Nord-Est, Adriatico Nord/Centro
|
|
||||||
if lat >= 43.0:
|
|
||||||
return "icon_d2", "ICON-D2 (2km)"
|
|
||||||
# ZONA 3: Sud Italia -> ICON-EU (Meglio di GFS per locale)
|
|
||||||
return "icon_eu", "ICON-EU (7km)"
|
|
||||||
|
|
||||||
# 5. RESTO DEL MONDO (Europa Centrale ICON-D2)
|
|
||||||
if cc in ['DE', 'AT', 'CH', 'LI']:
|
|
||||||
return "icon_d2", "ICON-D2"
|
|
||||||
|
|
||||||
# 6. RESTO DEL MONDO -> NOAA GFS
|
|
||||||
# Usiamo GFS invece di ECMWF perché ECMWF spesso manca di 'visibility'/'cape'
|
|
||||||
# facendo crashare o svuotare il report. GFS è completo.
|
|
||||||
return "gfs_global", "NOAA GFS Global"
|
|
||||||
|
|
||||||
def get_forecast(lat, lon, model):
|
|
||||||
params = {
|
|
||||||
"latitude": lat, "longitude": lon, "timezone": TZ,
|
|
||||||
"forecast_days": 3,
|
|
||||||
"models": model,
|
|
||||||
"wind_speed_unit": "kmh", "precipitation_unit": "mm",
|
|
||||||
"hourly": "temperature_2m,relative_humidity_2m,cloudcover,windspeed_10m,winddirection_10m,windgusts_10m,precipitation,rain,snowfall,weathercode,is_day,cape,visibility"
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=25)
|
|
||||||
r.raise_for_status()
|
|
||||||
return r.json()
|
|
||||||
except Exception as e: logger.error(f"Meteo API error: {e}"); return None
|
|
||||||
|
|
||||||
def safe_get_list(hourly_data, key, length, default=None):
|
|
||||||
"""Estrae una lista sicura, gestendo chiavi mancanti"""
|
|
||||||
if key in hourly_data and hourly_data[key] is not None:
|
|
||||||
return hourly_data[key]
|
|
||||||
return [default] * length
|
|
||||||
|
|
||||||
def generate_weather_report(lat, lon, location_name, cc="IT") -> str:
|
|
||||||
model_id, model_name = choose_best_model(lat, lon, cc)
|
|
||||||
|
|
||||||
data = get_forecast(lat, lon, model_id)
|
|
||||||
if not data: return f"❌ Errore API Meteo ({model_name})."
|
|
||||||
|
|
||||||
hourly = data.get("hourly", {})
|
|
||||||
times = hourly.get("time", [])
|
|
||||||
if not times: return "❌ Dati orari mancanti."
|
|
||||||
|
|
||||||
L = len(times)
|
|
||||||
|
|
||||||
try:
|
|
||||||
l_temp = safe_get_list(hourly, "temperature_2m", L, 0)
|
|
||||||
l_rh = safe_get_list(hourly, "relative_humidity_2m", L, 0)
|
|
||||||
l_cl = safe_get_list(hourly, "cloudcover", L, 0)
|
|
||||||
l_prec = safe_get_list(hourly, "precipitation", L, 0)
|
|
||||||
l_rain = safe_get_list(hourly, "rain", L, 0)
|
|
||||||
l_snow = safe_get_list(hourly, "snowfall", L, 0)
|
|
||||||
l_wspd = safe_get_list(hourly, "windspeed_10m", L, 0)
|
|
||||||
l_gust = safe_get_list(hourly, "windgusts_10m", L, 0)
|
|
||||||
l_wdir = safe_get_list(hourly, "winddirection_10m", L, 0)
|
|
||||||
l_code = safe_get_list(hourly, "weathercode", L, 0)
|
|
||||||
l_day = safe_get_list(hourly, "is_day", L, 1)
|
|
||||||
l_cape = safe_get_list(hourly, "cape", L, 0)
|
|
||||||
l_vis = safe_get_list(hourly, "visibility", L, 10000)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return f"❌ Errore elaborazione dati meteo: {e}"
|
|
||||||
|
|
||||||
now = now_local().replace(minute=0, second=0, microsecond=0)
|
|
||||||
blocks = []
|
|
||||||
|
|
||||||
for (label, hours_duration, step) in [("Prime 24h", 24, 1), ("Successive 24h", 24, 2)]:
|
|
||||||
end_time = now + datetime.timedelta(hours=hours_duration)
|
|
||||||
lines = [f"{'LT':<2} {'T°':>3} {'h%':>3} {'mm':>2} {'Vento':<5} {'Nv%':>3} {'Sky':<2} {'Sgx':<3}", "-" * 30]
|
|
||||||
count = 0
|
|
||||||
for i, t_str in enumerate(times):
|
|
||||||
try: dt = parse_time(t_str)
|
|
||||||
except: continue
|
|
||||||
if dt < now or dt >= end_time: continue
|
|
||||||
if dt.hour % step != 0: continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
T = float(l_temp[i])
|
|
||||||
Rh = int(l_rh[i] or 0)
|
|
||||||
Cl = int(l_cl[i] or 0)
|
|
||||||
Pr = float(l_prec[i] or 0)
|
|
||||||
Rn = float(l_rain[i] or 0)
|
|
||||||
Sn = float(l_snow[i] or 0)
|
|
||||||
Wspd = float(l_wspd[i] or 0)
|
|
||||||
Gust = float(l_gust[i] or 0)
|
|
||||||
Wdir = int(l_wdir[i] or 0)
|
|
||||||
Cape = float(l_cape[i] or 0)
|
|
||||||
Vis = float(l_vis[i] or 10000)
|
|
||||||
Code = int(l_code[i]) if l_code[i] is not None else None
|
|
||||||
IsDay = int(l_day[i] if l_day[i] is not None else 1)
|
|
||||||
|
|
||||||
t_s = f"{int(round(T))}"
|
|
||||||
p_s = "--" if Pr < 0.2 else f"{int(round(Pr))}"
|
|
||||||
card = degrees_to_cardinal(Wdir)
|
|
||||||
w_val = Gust if (Gust - Wspd) > 15 else Wspd
|
|
||||||
w_txt = f"{card} {int(round(w_val))}"
|
|
||||||
if (Gust - Wspd) > 15:
|
|
||||||
g_txt = f"G{int(round(w_val))}"
|
|
||||||
if len(card)+len(g_txt) <= 5: w_txt = f"{card}{g_txt}"
|
|
||||||
elif len(card)+1+len(g_txt) <= 5: w_txt = f"{card} {g_txt}"
|
|
||||||
else: w_txt = g_txt
|
|
||||||
w_fmt = f"{w_txt:<5}"
|
|
||||||
sky, sgx = get_icon_set(Pr, Sn, Code, IsDay, Cl, Vis, T, Rn, Gust, Cape)
|
|
||||||
lines.append(f"{dt.strftime('%H'):<2} {t_s:>3} {Rh:>3} {p_s:>2} {w_fmt} {Cl:>3} {sky:<2} {sgx:<3}")
|
|
||||||
count += 1
|
|
||||||
except: continue
|
|
||||||
if count > 0:
|
|
||||||
day_label = f"{['Lun','Mar','Mer','Gio','Ven','Sab','Dom'][now.weekday()]} {now.day}"
|
|
||||||
blocks.append(f"*{day_label} ({label})*\n```text\n" + "\n".join(lines) + "\n```")
|
|
||||||
now = end_time
|
|
||||||
|
|
||||||
return f"🌤️ *METEO REPORT*\n📍 {location_name}\n🧠 Fonte: {model_name}\n\n" + "\n\n".join(blocks)
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# SEZIONE 3: BOT HANDLERS & SCHEDULER
|
# SEZIONE 2: GESTORI COMANDI (HANDLERS)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# Decoratore Sicurezza Multi-Utente
|
# Decoratore Sicurezza
|
||||||
def restricted(func):
|
def restricted(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
async def wrapped(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs):
|
async def wrapped(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs):
|
||||||
user_id = update.effective_user.id
|
user_id = update.effective_user.id
|
||||||
if user_id not in ALLOWED_IDS:
|
if user_id not in ALLOWED_IDS: return
|
||||||
logger.warning(f"⚠️ ACCESSO NEGATO: User {user_id}")
|
|
||||||
return
|
|
||||||
return await func(update, context, *args, **kwargs)
|
return await func(update, context, *args, **kwargs)
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
@@ -345,53 +140,161 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||||||
keyboard = [
|
keyboard = [
|
||||||
[InlineKeyboardButton("🖥️ Core Server", callback_data="menu_core"), InlineKeyboardButton("🔍 Scan LAN", callback_data="menu_lan")],
|
[InlineKeyboardButton("🖥️ Core Server", callback_data="menu_core"), InlineKeyboardButton("🔍 Scan LAN", callback_data="menu_lan")],
|
||||||
[InlineKeyboardButton("🛡️ Pi-hole", callback_data="menu_pihole"), InlineKeyboardButton("🌐 Rete", callback_data="menu_net")],
|
[InlineKeyboardButton("🛡️ Pi-hole", callback_data="menu_pihole"), InlineKeyboardButton("🌐 Rete", callback_data="menu_net")],
|
||||||
[InlineKeyboardButton("🌤️ Meteo Casa", callback_data="req_meteo_home"), InlineKeyboardButton("📜 Logs", callback_data="menu_logs")]
|
[InlineKeyboardButton("🌤️ Meteo Casa", callback_data="req_meteo_home"), InlineKeyboardButton("📹 Camere", callback_data="menu_cams")],
|
||||||
|
[InlineKeyboardButton("📜 Logs", callback_data="menu_logs")]
|
||||||
]
|
]
|
||||||
text = "🎛 **Loogle Control Center v7.9**\nComandi disponibili:\n🔹 `/meteo <città>`\n🔹 Pulsanti sotto"
|
text = "🎛 **Loogle Control Center v9.0**\n\n🔹 `/meteo <città>`\n🔹 `/meteo7 <città>` (7 Giorni)\n🔹 `/cam <nome>` (Snapshot)"
|
||||||
|
|
||||||
if update.message: await update.message.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
|
if update.message: await update.message.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
|
||||||
else: await update.callback_query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
|
else: await update.callback_query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
|
||||||
|
|
||||||
@restricted
|
@restricted
|
||||||
async def meteo_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def meteo_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
query = " ".join(context.args).strip()
|
||||||
|
if not query or query.lower() == "casa":
|
||||||
|
await update.message.reply_text("⏳ **Scarico Meteo Casa...**", parse_mode="Markdown")
|
||||||
|
report = call_script_text(METEO_SCRIPT, ["--home"])
|
||||||
|
else:
|
||||||
|
await update.message.reply_text(f"🔄 Cerco '{query}'...", parse_mode="Markdown")
|
||||||
|
report = call_script_text(METEO_SCRIPT, ["--query", query])
|
||||||
|
|
||||||
|
await update.message.reply_text(report, parse_mode="Markdown")
|
||||||
|
|
||||||
|
@restricted
|
||||||
|
async def meteo7_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
chat_id = update.effective_chat.id
|
||||||
|
query = "casa"
|
||||||
|
if context.args: query = " ".join(context.args)
|
||||||
|
await update.message.reply_text(f"📡 Calcolo previsione 7gg per: {query}...", parse_mode="Markdown")
|
||||||
|
subprocess.Popen(["python3", METEO7_SCRIPT, query, "--chat_id", str(chat_id)])
|
||||||
|
|
||||||
|
@restricted
|
||||||
|
async def cam_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
if not context.args:
|
if not context.args:
|
||||||
await update.message.reply_text("⚠️ Usa: `/meteo <città>` (es. `/meteo Rimini`)", parse_mode="Markdown")
|
# Se non c'è argomento, mostra il menu camere
|
||||||
|
keyboard = [
|
||||||
|
[InlineKeyboardButton("📷 Sala", callback_data="req_cam_sala"), InlineKeyboardButton("📷 Ingresso", callback_data="req_cam_ingresso")],
|
||||||
|
[InlineKeyboardButton("📷 Taverna", callback_data="req_cam_taverna"), InlineKeyboardButton("📷 Retro", callback_data="req_cam_retro")],
|
||||||
|
[InlineKeyboardButton("📷 Matrim.", callback_data="req_cam_matrimoniale"), InlineKeyboardButton("📷 Luca", callback_data="req_cam_luca")],
|
||||||
|
[InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]
|
||||||
|
]
|
||||||
|
await update.message.reply_text("📹 **Scegli una telecamera:**", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
|
||||||
return
|
return
|
||||||
|
|
||||||
city = " ".join(context.args)
|
cam_name = context.args[0]
|
||||||
await update.message.reply_text(f"🔄 Cerco '{city}'...", parse_mode="Markdown")
|
await update.message.reply_chat_action(action="upload_photo")
|
||||||
|
|
||||||
coords = get_coordinates(city)
|
try:
|
||||||
if coords:
|
# Timeout 15s per RTSP
|
||||||
lat, lon, name, cc = coords
|
result = subprocess.run(["python3", CAM_SCRIPT, cam_name], capture_output=True, text=True, timeout=15)
|
||||||
report = generate_weather_report(lat, lon, name, cc)
|
output = result.stdout.strip()
|
||||||
await update.message.reply_text(report, parse_mode="Markdown")
|
|
||||||
|
if output.startswith("OK:"):
|
||||||
|
img_path = output.split(":", 1)[1]
|
||||||
|
await update.message.reply_photo(photo=open(img_path, 'rb'), caption=f"📷 **{cam_name.capitalize()}**")
|
||||||
|
elif output.startswith("ERR:"):
|
||||||
|
await update.message.reply_text(output.split(":", 1)[1])
|
||||||
else:
|
else:
|
||||||
await update.message.reply_text(f"❌ Città '{city}' non trovata.", parse_mode="Markdown")
|
await update.message.reply_text(f"❌ Risposta imprevista dallo script: {output}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await update.message.reply_text(f"❌ Errore critico: {e}")
|
||||||
|
|
||||||
async def scheduled_morning_report(context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def scheduled_morning_report(context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
logger.info("⏰ Invio report automatico meteo...")
|
# Meteo automatico alle 8:00
|
||||||
# Forza "SM" per casa -> Arome/IconD2 in base alla posizione
|
report = call_script_text(METEO_SCRIPT, ["--home"])
|
||||||
report = generate_weather_report(HOME_LAT, HOME_LON, HOME_NAME, "SM")
|
|
||||||
for uid in ALLOWED_IDS:
|
for uid in ALLOWED_IDS:
|
||||||
try: await context.bot.send_message(chat_id=uid, text=report, parse_mode="Markdown")
|
try: await context.bot.send_message(chat_id=uid, text=report, parse_mode="Markdown")
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
|
@restricted
|
||||||
|
async def clip_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
if not context.args:
|
||||||
|
await update.message.reply_text("⚠️ Usa: `/clip <nome_camera>` (es. /clip sala)", parse_mode="Markdown")
|
||||||
|
return
|
||||||
|
|
||||||
|
cam_name = context.args[0]
|
||||||
|
await update.message.reply_chat_action(action="upload_video") # Icona "sta inviando video..."
|
||||||
|
await update.message.reply_text(f"🎥 **Registro 10s da {cam_name}...**", parse_mode="Markdown")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Lancia lo script con flag --video
|
||||||
|
result = subprocess.run(["python3", CAM_SCRIPT, cam_name, "--video"], capture_output=True, text=True, timeout=20)
|
||||||
|
output = result.stdout.strip()
|
||||||
|
|
||||||
|
if output.startswith("OK:"):
|
||||||
|
vid_path = output.split(":", 1)[1]
|
||||||
|
await update.message.reply_video(video=open(vid_path, 'rb'), caption=f"🎥 **Clip: {cam_name.capitalize()}**")
|
||||||
|
elif output.startswith("ERR:"):
|
||||||
|
await update.message.reply_text(output.split(":", 1)[1])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await update.message.reply_text(f"❌ Errore critico: {e}")
|
||||||
|
|
||||||
@restricted
|
@restricted
|
||||||
async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
await query.answer() # Risposta immediata per togliere il loading dal pulsante
|
||||||
data = query.data
|
data = query.data
|
||||||
|
|
||||||
if data == "main_menu": await start(update, context)
|
# --- NAVIGAZIONE MENU ---
|
||||||
|
if data == "main_menu":
|
||||||
|
await start(update, context)
|
||||||
|
|
||||||
|
# --- SEZIONE METEO ---
|
||||||
elif data == "req_meteo_home":
|
elif data == "req_meteo_home":
|
||||||
await query.edit_message_text("⏳ **Scaricamento Meteo Casa...**", parse_mode="Markdown")
|
await query.edit_message_text("⏳ **Scaricamento Meteo Casa...**", parse_mode="Markdown")
|
||||||
# Forza "SM" per casa
|
report = call_script_text(METEO_SCRIPT, ["--home"])
|
||||||
report = generate_weather_report(HOME_LAT, HOME_LON, HOME_NAME, "SM")
|
|
||||||
keyboard = [[InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]]
|
keyboard = [[InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]]
|
||||||
await query.edit_message_text(report, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
|
await query.edit_message_text(report, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
|
||||||
|
|
||||||
|
# --- SEZIONE CAMERE ---
|
||||||
|
elif data == "menu_cams":
|
||||||
|
keyboard = [
|
||||||
|
[InlineKeyboardButton("📷 Sala", callback_data="req_cam_sala"), InlineKeyboardButton("📷 Ingresso", callback_data="req_cam_ingresso")],
|
||||||
|
[InlineKeyboardButton("📷 Taverna", callback_data="req_cam_taverna"), InlineKeyboardButton("📷 Retro", callback_data="req_cam_retro")],
|
||||||
|
[InlineKeyboardButton("📷 Matrim.", callback_data="req_cam_matrimoniale"), InlineKeyboardButton("📷 Luca", callback_data="req_cam_luca")],
|
||||||
|
[InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]
|
||||||
|
]
|
||||||
|
await query.edit_message_text("📹 **Centrale Video**\nSeleziona una telecamera:", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
|
||||||
|
|
||||||
|
elif data.startswith("req_cam_"):
|
||||||
|
cam_name = data.replace("req_cam_", "")
|
||||||
|
# Non editiamo il messaggio, inviamo una nuova foto sotto
|
||||||
|
try:
|
||||||
|
result = subprocess.run(["python3", CAM_SCRIPT, cam_name], capture_output=True, text=True, timeout=15)
|
||||||
|
output = result.stdout.strip()
|
||||||
|
|
||||||
|
if output.startswith("OK:"):
|
||||||
|
img_path = output.split(":", 1)[1]
|
||||||
|
await context.bot.send_photo(chat_id=update.effective_chat.id, photo=open(img_path, 'rb'), caption=f"📷 **{cam_name.capitalize()}**")
|
||||||
|
elif output.startswith("ERR:"):
|
||||||
|
await context.bot.send_message(chat_id=update.effective_chat.id, text=output.split(":", 1)[1])
|
||||||
|
except Exception as e:
|
||||||
|
await context.bot.send_message(chat_id=update.effective_chat.id, text=f"❌ Errore richiesta cam: {e}")
|
||||||
|
|
||||||
|
elif data.startswith("req_vid_"):
|
||||||
|
cam_name = data.replace("req_vid_", "")
|
||||||
|
await query.answer("🎥 Registrazione in corso (10s)...")
|
||||||
|
# Inviamo un messaggio di attesa perché ci mette un po'
|
||||||
|
msg = await context.bot.send_message(chat_id=update.effective_chat.id, text=f"⏳ Registro clip: {cam_name}...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(["python3", CAM_SCRIPT, cam_name, "--video"], capture_output=True, text=True, timeout=20)
|
||||||
|
output = result.stdout.strip()
|
||||||
|
|
||||||
|
# Cancelliamo il messaggio di attesa
|
||||||
|
await context.bot.delete_message(chat_id=update.effective_chat.id, message_id=msg.message_id)
|
||||||
|
|
||||||
|
if output.startswith("OK:"):
|
||||||
|
vid_path = output.split(":", 1)[1]
|
||||||
|
await context.bot.send_video(chat_id=update.effective_chat.id, video=open(vid_path, 'rb'), caption=f"🎥 **Clip: {cam_name.capitalize()}**")
|
||||||
|
elif output.startswith("ERR:"):
|
||||||
|
await context.bot.send_message(chat_id=update.effective_chat.id, text=output.split(":", 1)[1])
|
||||||
|
except Exception as e:
|
||||||
|
await context.bot.send_message(chat_id=update.effective_chat.id, text=f"❌ Errore: {e}")
|
||||||
|
|
||||||
|
# --- SEZIONE SISTEMA CORE ---
|
||||||
elif data == "menu_core":
|
elif data == "menu_core":
|
||||||
keyboard = []
|
keyboard = []
|
||||||
for i, dev in enumerate(CORE_DEVICES): keyboard.append([InlineKeyboardButton(dev['name'], callback_data=f"stat_{i}")])
|
for i, dev in enumerate(CORE_DEVICES): keyboard.append([InlineKeyboardButton(dev['name'], callback_data=f"stat_{i}")])
|
||||||
@@ -410,6 +313,7 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
await query.edit_message_text(f"⏳ Controllo {dev['name']}...", parse_mode="Markdown")
|
await query.edit_message_text(f"⏳ Controllo {dev['name']}...", parse_mode="Markdown")
|
||||||
await query.edit_message_text(f"🔹 **{dev['name']}**\n\n{get_device_stats(dev)}", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_core")]]), parse_mode="Markdown")
|
await query.edit_message_text(f"🔹 **{dev['name']}**\n\n{get_device_stats(dev)}", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_core")]]), parse_mode="Markdown")
|
||||||
|
|
||||||
|
# --- SEZIONE LAN ---
|
||||||
elif data == "menu_lan":
|
elif data == "menu_lan":
|
||||||
await query.edit_message_text("⏳ **Scansione LAN...**", parse_mode="Markdown")
|
await query.edit_message_text("⏳ **Scansione LAN...**", parse_mode="Markdown")
|
||||||
report = "🔍 **DIAGNOSTICA LAN**\n\n"
|
report = "🔍 **DIAGNOSTICA LAN**\n\n"
|
||||||
@@ -433,6 +337,7 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
res = run_cmd("reboot", dev['ip'], "admin")
|
res = run_cmd("reboot", dev['ip'], "admin")
|
||||||
await query.edit_message_text(f"⚡ Inviato a {dev['name']}...\n\n`{res}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_reboot")]]), parse_mode="Markdown")
|
await query.edit_message_text(f"⚡ Inviato a {dev['name']}...\n\n`{res}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_reboot")]]), parse_mode="Markdown")
|
||||||
|
|
||||||
|
# --- SEZIONE PI-HOLE ---
|
||||||
elif data == "menu_pihole":
|
elif data == "menu_pihole":
|
||||||
status_raw = run_cmd("sudo pihole status", MASTER_IP, SSH_USER)
|
status_raw = run_cmd("sudo pihole status", MASTER_IP, SSH_USER)
|
||||||
icon = "✅" if "Enabled" in status_raw or "enabled" in status_raw else "🔴"
|
icon = "✅" if "Enabled" in status_raw or "enabled" in status_raw else "🔴"
|
||||||
@@ -446,16 +351,18 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
elif "restart" in data: run_cmd("sudo systemctl restart pihole-FTL", MASTER_IP, SSH_USER)
|
elif "restart" in data: run_cmd("sudo systemctl restart pihole-FTL", MASTER_IP, SSH_USER)
|
||||||
await query.edit_message_text("✅ Comando inviato.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_pihole")]]), parse_mode="Markdown")
|
await query.edit_message_text("✅ Comando inviato.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_pihole")]]), parse_mode="Markdown")
|
||||||
|
|
||||||
|
# --- SEZIONE RETE ---
|
||||||
elif data == "menu_net":
|
elif data == "menu_net":
|
||||||
ip = run_cmd("curl -s ifconfig.me")
|
ip = run_cmd("curl -s ifconfig.me")
|
||||||
keyboard = [[InlineKeyboardButton("🚀 Speedtest", callback_data="net_speedtest")], [InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]]
|
keyboard = [[InlineKeyboardButton("🚀 Speedtest", callback_data="net_speedtest")], [InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]]
|
||||||
await query.edit_message_text(f"🌐 **Rete**\n🌍 IP: `{ip}`", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
|
await query.edit_message_text(f"🌐 **Rete**\n🌍 IP: `{ip}`", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
|
||||||
|
|
||||||
elif data == "net_speedtest":
|
elif data == "net_speedtest":
|
||||||
await query.edit_message_text("🚀 **Speedtest...**", parse_mode="Markdown")
|
await query.edit_message_text("🚀 **Speedtest... (attendi 40s)**", parse_mode="Markdown")
|
||||||
res = run_speedtest()
|
res = run_speedtest()
|
||||||
await query.edit_message_text(f"🚀 **Risultato:**\n\n`{res}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_net")]]), parse_mode="Markdown")
|
await query.edit_message_text(f"🚀 **Risultato:**\n\n`{res}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_net")]]), parse_mode="Markdown")
|
||||||
|
|
||||||
|
# --- SEZIONE LOGS ---
|
||||||
elif data == "menu_logs":
|
elif data == "menu_logs":
|
||||||
keyboard = [[InlineKeyboardButton("🐶 Watchdog", callback_data="log_wd"), InlineKeyboardButton("💾 Backup", callback_data="log_bk")], [InlineKeyboardButton("🔄 NPM Sync", callback_data="log_npm"), InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]]
|
keyboard = [[InlineKeyboardButton("🐶 Watchdog", callback_data="log_wd"), InlineKeyboardButton("💾 Backup", callback_data="log_bk")], [InlineKeyboardButton("🔄 NPM Sync", callback_data="log_npm"), InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]]
|
||||||
await query.edit_message_text("📜 **Logs**", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
|
await query.edit_message_text("📜 **Logs**", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
|
||||||
@@ -466,15 +373,20 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
await query.edit_message_text(f"📜 **Log:**\n\n`{log_c}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_logs")]]), parse_mode="Markdown")
|
await query.edit_message_text(f"📜 **Log:**\n\n`{log_c}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_logs")]]), parse_mode="Markdown")
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
logger.info("Avvio Loogle Bot v7.9 (Global GFS Fix)...")
|
logger.info("Avvio Loogle Bot v9.0 (Modular)...")
|
||||||
application = Application.builder().token(BOT_TOKEN).build()
|
application = Application.builder().token(BOT_TOKEN).build()
|
||||||
|
|
||||||
# Handlers
|
# Registrazione Comandi
|
||||||
application.add_handler(CommandHandler("start", start))
|
application.add_handler(CommandHandler("start", start))
|
||||||
application.add_handler(CommandHandler("meteo", meteo_command))
|
application.add_handler(CommandHandler("meteo", meteo_command))
|
||||||
|
application.add_handler(CommandHandler("meteo7", meteo7_command))
|
||||||
|
application.add_handler(CommandHandler("cam", cam_command))
|
||||||
|
application.add_handler(CommandHandler("clip", clip_command))
|
||||||
|
|
||||||
|
# Registrazione Callback Menu
|
||||||
application.add_handler(CallbackQueryHandler(button_handler))
|
application.add_handler(CallbackQueryHandler(button_handler))
|
||||||
|
|
||||||
# SCHEDULER
|
# Scheduler
|
||||||
job_queue = application.job_queue
|
job_queue = application.job_queue
|
||||||
job_queue.run_daily(scheduled_morning_report, time=datetime.time(hour=8, minute=0, tzinfo=TZINFO), days=(0, 1, 2, 3, 4, 5, 6))
|
job_queue.run_daily(scheduled_morning_report, time=datetime.time(hour=8, minute=0, tzinfo=TZINFO), days=(0, 1, 2, 3, 4, 5, 6))
|
||||||
|
|
||||||
|
|||||||
97
services/telegram-bot/cam.py
Executable file
97
services/telegram-bot/cam.py
Executable file
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# --- CONFIGURAZIONE HIKVISION ---
|
||||||
|
RTSP_USER = "admin"
|
||||||
|
RTSP_PASS = "@Dedelove1"
|
||||||
|
RTSP_PORT = "554"
|
||||||
|
RTSP_PATH = "/Streaming/channels/101"
|
||||||
|
|
||||||
|
def get_url(ip):
|
||||||
|
return f"rtsp://{RTSP_USER}:{RTSP_PASS}@{ip}:{RTSP_PORT}{RTSP_PATH}"
|
||||||
|
|
||||||
|
CAM_CONFIG = {
|
||||||
|
"matrimoniale": get_url("192.168.135.2"),
|
||||||
|
"luca": get_url("192.168.135.3"),
|
||||||
|
"ingresso": get_url("192.168.135.4"),
|
||||||
|
"sala": get_url("192.168.135.5"),
|
||||||
|
"taverna": get_url("192.168.135.6"),
|
||||||
|
"retro": get_url("192.168.135.7"),
|
||||||
|
}
|
||||||
|
|
||||||
|
OUTPUT_PHOTO = "/tmp/cam_snapshot.jpg"
|
||||||
|
OUTPUT_VIDEO = "/tmp/cam_video.mp4"
|
||||||
|
|
||||||
|
def get_cam_key(cam_name):
|
||||||
|
cam_key = cam_name.lower().strip()
|
||||||
|
if cam_key.startswith("cam "): cam_key = cam_key.replace("cam ", "")
|
||||||
|
|
||||||
|
if cam_key in CAM_CONFIG: return cam_key
|
||||||
|
|
||||||
|
for key in CAM_CONFIG:
|
||||||
|
if key in cam_key: return key
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_media(cam_name, is_video=False):
|
||||||
|
key = get_cam_key(cam_name)
|
||||||
|
if not key:
|
||||||
|
return None, f"❌ Camera '{cam_name}' non trovata.\nDisponibili: {', '.join(CAM_CONFIG.keys())}"
|
||||||
|
|
||||||
|
rtsp_url = CAM_CONFIG[key]
|
||||||
|
output_file = OUTPUT_VIDEO if is_video else OUTPUT_PHOTO
|
||||||
|
|
||||||
|
if is_video:
|
||||||
|
# COMANDO VIDEO (10 SECONDI)
|
||||||
|
# -t 10: Durata 10 secondi
|
||||||
|
# -c:v copy: COPIA il flusso video senza ricodificarlo (Zero CPU, Istantaneo)
|
||||||
|
# Se Telegram non legge il video, cambia "-c:v copy" in "-c:v libx264 -preset ultrafast"
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg", "-y",
|
||||||
|
"-rtsp_transport", "tcp",
|
||||||
|
"-i", rtsp_url,
|
||||||
|
"-t", "10", # Durata clip
|
||||||
|
"-c:v", "libx264", # Ricodifica leggera per compatibilità Telegram garantita
|
||||||
|
"-preset", "ultrafast", # Velocissimo per non caricare la CPU
|
||||||
|
"-an", # Rimuovi Audio (togli questa riga se vuoi l'audio)
|
||||||
|
"-f", "mp4",
|
||||||
|
output_file
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
# COMANDO FOTO
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg", "-y",
|
||||||
|
"-rtsp_transport", "tcp",
|
||||||
|
"-i", rtsp_url,
|
||||||
|
"-frames:v", "1",
|
||||||
|
"-q:v", "2",
|
||||||
|
output_file
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=15, check=True)
|
||||||
|
|
||||||
|
if os.path.exists(output_file) and os.path.getsize(output_file) > 0:
|
||||||
|
return output_file, None
|
||||||
|
else:
|
||||||
|
return None, "❌ Errore: File output vuoto."
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return None, "⏰ Timeout: La cam non risponde."
|
||||||
|
except Exception as e:
|
||||||
|
return None, f"❌ Errore: {e}"
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("name", help="Nome della camera")
|
||||||
|
parser.add_argument("--video", action="store_true", help="Registra video clip invece di foto")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
path, error = get_media(args.name, args.video)
|
||||||
|
|
||||||
|
if path:
|
||||||
|
print(f"OK:{path}")
|
||||||
|
else:
|
||||||
|
print(f"ERR:{error}")
|
||||||
352
services/telegram-bot/meteo.py
Normal file
352
services/telegram-bot/meteo.py
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import requests
|
||||||
|
import datetime
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
from dateutil import parser as date_parser # pyright: ignore[reportMissingModuleSource]
|
||||||
|
|
||||||
|
# Setup logging
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# --- CONFIGURAZIONE METEO ---
|
||||||
|
HOME_LAT = 43.9356
|
||||||
|
HOME_LON = 12.4296
|
||||||
|
HOME_NAME = "🏠 Casa (Wide View ±12km)"
|
||||||
|
TZ = "Europe/Rome"
|
||||||
|
TZINFO = ZoneInfo(TZ)
|
||||||
|
|
||||||
|
# Offset ~12-15km
|
||||||
|
OFFSET_LAT = 0.12
|
||||||
|
OFFSET_LON = 0.16
|
||||||
|
|
||||||
|
OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"
|
||||||
|
GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search"
|
||||||
|
HTTP_HEADERS = {"User-Agent": "loogle-bot-v10.4"}
|
||||||
|
|
||||||
|
def now_local() -> datetime.datetime:
|
||||||
|
return datetime.datetime.now(TZINFO)
|
||||||
|
|
||||||
|
def parse_time(t: str) -> datetime.datetime:
|
||||||
|
try:
|
||||||
|
dt = date_parser.isoparse(t)
|
||||||
|
if dt.tzinfo is None: return dt.replace(tzinfo=TZINFO)
|
||||||
|
return dt.astimezone(TZINFO)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Time parse error: {e}")
|
||||||
|
return now_local()
|
||||||
|
|
||||||
|
def degrees_to_cardinal(d: int) -> str:
|
||||||
|
dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
|
||||||
|
try:
|
||||||
|
return dirs[round(d / 45) % 8]
|
||||||
|
except: return "N"
|
||||||
|
|
||||||
|
def get_icon_set(prec, snow, code, is_day, cloud, vis, temp, rain, gust, cape, cloud_type):
|
||||||
|
sky = "☁️"
|
||||||
|
try:
|
||||||
|
if cloud_type == 'F':
|
||||||
|
sky = "🌫️"
|
||||||
|
elif code in (95, 96, 99): sky = "⛈️" if prec > 0 else "🌩️"
|
||||||
|
elif prec >= 0.1: sky = "🌨️" if snow > 0 else "🌧️"
|
||||||
|
else:
|
||||||
|
# LOGICA PERCEZIONE UMANA
|
||||||
|
if cloud_type == 'H':
|
||||||
|
if cloud <= 40: sky = "☀️" if is_day else "🌙"
|
||||||
|
elif cloud <= 80: sky = "🌤️" if is_day else "🌙"
|
||||||
|
else: sky = "🌥️"
|
||||||
|
else:
|
||||||
|
if cloud <= 15: sky = "☀️" if is_day else "🌙"
|
||||||
|
elif cloud <= 35: sky = "🌤️" if is_day else "🌙"
|
||||||
|
elif cloud <= 60: sky = "⛅️"
|
||||||
|
elif cloud <= 85: sky = "🌥️"
|
||||||
|
else: sky = "☁️"
|
||||||
|
|
||||||
|
sgx = "-"
|
||||||
|
if snow > 0 or (code is not None and code in (71,73,75,77,85,86)): sgx = "☃️"
|
||||||
|
elif temp < 0 or (code is not None and code in (66,67)): sgx = "🧊"
|
||||||
|
elif cape > 2000: sgx = "🌪️"
|
||||||
|
elif cape > 1000: sgx = "⚡"
|
||||||
|
elif temp > 35: sgx = "🥵"
|
||||||
|
elif rain > 4: sgx = "☔️"
|
||||||
|
elif gust > 50: sgx = "💨"
|
||||||
|
|
||||||
|
return sky, sgx
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Icon error: {e}")
|
||||||
|
return "❓", "-"
|
||||||
|
|
||||||
|
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)
|
||||||
|
data = r.json()
|
||||||
|
if "results" in data and data["results"]:
|
||||||
|
res = data["results"][0]
|
||||||
|
cc = res.get("country_code", "IT").upper()
|
||||||
|
name = f"{res.get('name')} ({cc})"
|
||||||
|
return res["latitude"], res["longitude"], name, cc
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Geocoding error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def choose_best_model(lat, lon, cc):
|
||||||
|
if cc == 'JP': return "jma_msm", "JMA MSM"
|
||||||
|
if cc in ['NO', 'SE', 'FI', 'DK', 'IS']: return "metno_nordic", "Yr.no"
|
||||||
|
if cc in ['GB', 'IE']: return "ukmo_global", "UK MetOffice"
|
||||||
|
if cc == 'IT' or cc == 'SM': return "meteofrance_arome_france_hd", "AROME HD"
|
||||||
|
if cc in ['DE', 'AT', 'CH', 'LI', 'FR']: return "icon_d2", "ICON-D2"
|
||||||
|
return "gfs_global", "NOAA GFS"
|
||||||
|
|
||||||
|
def get_forecast(lat, lon, model):
|
||||||
|
lats = [lat, lat + OFFSET_LAT, lat - OFFSET_LAT, lat, lat]
|
||||||
|
lons = [lon, lon, lon, lon + OFFSET_LON, lon - OFFSET_LON]
|
||||||
|
|
||||||
|
lat_str = ",".join(map(str, lats))
|
||||||
|
lon_str = ",".join(map(str, lons))
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"latitude": lat_str, "longitude": lon_str, "timezone": TZ,
|
||||||
|
"forecast_days": 3,
|
||||||
|
"models": model,
|
||||||
|
"wind_speed_unit": "kmh", "precipitation_unit": "mm",
|
||||||
|
"hourly": "temperature_2m,apparent_temperature,relative_humidity_2m,cloud_cover,cloud_cover_low,cloud_cover_mid,cloud_cover_high,windspeed_10m,winddirection_10m,windgusts_10m,precipitation,rain,snowfall,weathercode,is_day,cape,visibility,uv_index"
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=25)
|
||||||
|
if r.status_code != 200:
|
||||||
|
logger.error(f"API Error {r.status_code}: {r.text}")
|
||||||
|
return None
|
||||||
|
return r.json()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Request error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def safe_get_list(hourly_data, key, length, default=None):
|
||||||
|
if key in hourly_data and hourly_data[key] is not None:
|
||||||
|
return hourly_data[key]
|
||||||
|
return [default] * length
|
||||||
|
|
||||||
|
def get_val(val, default=0.0):
|
||||||
|
if val is None: return default
|
||||||
|
return float(val)
|
||||||
|
|
||||||
|
def generate_weather_report(lat, lon, location_name, debug_mode=False, cc="IT") -> str:
|
||||||
|
model_id, model_name = choose_best_model(lat, lon, cc)
|
||||||
|
|
||||||
|
data_list = get_forecast(lat, lon, model_id)
|
||||||
|
if not data_list: return f"❌ Errore API Meteo ({model_name})."
|
||||||
|
if not isinstance(data_list, list): data_list = [data_list]
|
||||||
|
|
||||||
|
data_center = data_list[0]
|
||||||
|
hourly_c = data_center.get("hourly", {})
|
||||||
|
times = hourly_c.get("time", [])
|
||||||
|
if not times: return "❌ Dati orari mancanti."
|
||||||
|
|
||||||
|
L = len(times)
|
||||||
|
|
||||||
|
# --- DATI LOCALI (CASA) ---
|
||||||
|
l_temp = safe_get_list(hourly_c, "temperature_2m", L, 0)
|
||||||
|
l_app = safe_get_list(hourly_c, "apparent_temperature", L, 0)
|
||||||
|
l_rh = safe_get_list(hourly_c, "relative_humidity_2m", L, 50)
|
||||||
|
l_prec = safe_get_list(hourly_c, "precipitation", L, 0)
|
||||||
|
l_rain = safe_get_list(hourly_c, "rain", L, 0)
|
||||||
|
l_snow = safe_get_list(hourly_c, "snowfall", L, 0)
|
||||||
|
l_wspd = safe_get_list(hourly_c, "windspeed_10m", L, 0)
|
||||||
|
l_gust = safe_get_list(hourly_c, "windgusts_10m", L, 0)
|
||||||
|
l_wdir = safe_get_list(hourly_c, "winddirection_10m", L, 0)
|
||||||
|
l_code = safe_get_list(hourly_c, "weathercode", L, 0)
|
||||||
|
l_day = safe_get_list(hourly_c, "is_day", L, 1)
|
||||||
|
l_cape = safe_get_list(hourly_c, "cape", L, 0)
|
||||||
|
l_vis = safe_get_list(hourly_c, "visibility", L, 10000)
|
||||||
|
l_uv = safe_get_list(hourly_c, "uv_index", L, 0)
|
||||||
|
|
||||||
|
# Estraggo anche i dati nuvole LOCALI per il tipo
|
||||||
|
l_cl_low_loc = safe_get_list(hourly_c, "cloud_cover_low", L, 0)
|
||||||
|
l_cl_mid_loc = safe_get_list(hourly_c, "cloud_cover_mid", L, 0)
|
||||||
|
l_cl_hig_loc = safe_get_list(hourly_c, "cloud_cover_high", L, 0)
|
||||||
|
|
||||||
|
# --- DATI GLOBALI (MEDIA) ---
|
||||||
|
acc_cl_tot = [0.0] * L
|
||||||
|
points_cl_tot = [ [] for _ in range(L) ]
|
||||||
|
p_names = ["Casa", "Nord", "Sud", "Est", "Ovest"]
|
||||||
|
|
||||||
|
for d in data_list:
|
||||||
|
h = d.get("hourly", {})
|
||||||
|
for i in range(L):
|
||||||
|
cc = get_val(safe_get_list(h, "cloud_cover", L)[i])
|
||||||
|
cl = get_val(safe_get_list(h, "cloud_cover_low", L)[i])
|
||||||
|
cm = get_val(safe_get_list(h, "cloud_cover_mid", L)[i])
|
||||||
|
ch = get_val(safe_get_list(h, "cloud_cover_high", L)[i])
|
||||||
|
|
||||||
|
real_point_total = max(cc, cl, cm, ch)
|
||||||
|
|
||||||
|
acc_cl_tot[i] += real_point_total
|
||||||
|
points_cl_tot[i].append(real_point_total)
|
||||||
|
|
||||||
|
num_points = len(data_list)
|
||||||
|
avg_cl_tot = [x / num_points for x in acc_cl_tot]
|
||||||
|
|
||||||
|
if debug_mode:
|
||||||
|
output = f"🔍 **DEBUG 5 PUNTI (V10.4)**\n"
|
||||||
|
now_h = now_local().replace(minute=0, second=0, microsecond=0)
|
||||||
|
idx = 0
|
||||||
|
for i, t_str in enumerate(times):
|
||||||
|
if parse_time(t_str) >= now_h:
|
||||||
|
idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# Valori Locali
|
||||||
|
loc_L = get_val(l_cl_low_loc[idx])
|
||||||
|
loc_H = get_val(l_cl_hig_loc[idx])
|
||||||
|
|
||||||
|
output += f"Ora: {parse_time(times[idx]).strftime('%H:%M')}\n"
|
||||||
|
output += f"📍 **LOCALE (Casa)**: L:{int(loc_L)}% | M:{int(get_val(l_cl_mid_loc[idx]))}% | H:{int(loc_H)}%\n"
|
||||||
|
output += f"🌍 **MEDIA GLOBALE**: {int(avg_cl_tot[idx])}%\n"
|
||||||
|
|
||||||
|
decision = "H"
|
||||||
|
if loc_L > 40: decision = "L (Priorità Locale)"
|
||||||
|
output += f"👉 **Decisione**: {decision}\n"
|
||||||
|
return output
|
||||||
|
|
||||||
|
now = now_local().replace(minute=0, second=0, microsecond=0)
|
||||||
|
blocks = []
|
||||||
|
header = f"{'LT':<2} {'T°':>4} {'h%':>3} {'mm':>3} {'Vento':<5} {'Nv%':>5} {'Sk':<2} {'Sx':<2}"
|
||||||
|
separator = "-" * 31
|
||||||
|
|
||||||
|
for (label, hours_duration, step) in [("Prime 24h", 24, 1), ("Successive 24h", 24, 2)]:
|
||||||
|
end_time = now + datetime.timedelta(hours=hours_duration)
|
||||||
|
lines = [header, separator]
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
for i, t_str in enumerate(times):
|
||||||
|
try:
|
||||||
|
dt = parse_time(t_str)
|
||||||
|
if dt < now or dt >= end_time: continue
|
||||||
|
if dt.hour % step != 0: continue
|
||||||
|
|
||||||
|
T = get_val(l_temp[i], 0)
|
||||||
|
App = get_val(l_app[i], 0)
|
||||||
|
Rh = int(get_val(l_rh[i], 50))
|
||||||
|
|
||||||
|
t_suffix = ""
|
||||||
|
diff = App - T
|
||||||
|
if diff <= -2.5: t_suffix = "W"
|
||||||
|
elif diff >= 2.5: t_suffix = "H"
|
||||||
|
t_s = f"{int(round(T))}{t_suffix}"
|
||||||
|
|
||||||
|
Pr = get_val(l_prec[i], 0)
|
||||||
|
Sn = get_val(l_snow[i], 0)
|
||||||
|
Code = int(l_code[i]) if l_code[i] is not None else 0
|
||||||
|
|
||||||
|
p_suffix = ""
|
||||||
|
if Code in [96, 99]: p_suffix = "G"
|
||||||
|
elif Code in [66, 67]: p_suffix = "Z"
|
||||||
|
elif Sn > 0 or Code in [71, 73, 75, 77, 85, 86]: p_suffix = "N"
|
||||||
|
p_s = "--" if Pr < 0.2 else f"{int(round(Pr))}{p_suffix}"
|
||||||
|
|
||||||
|
# --- CLOUD LOGIC (V10.4: LOCAL PRIORITY) ---
|
||||||
|
|
||||||
|
# Usiamo la MEDIA per la quantità (Panoramica)
|
||||||
|
c_avg_tot = int(avg_cl_tot[i])
|
||||||
|
|
||||||
|
# Usiamo i dati LOCALI per il tipo (Cosa ho sulla testa)
|
||||||
|
loc_L = get_val(l_cl_low_loc[i])
|
||||||
|
loc_M = get_val(l_cl_mid_loc[i])
|
||||||
|
loc_H = get_val(l_cl_hig_loc[i])
|
||||||
|
Vis = get_val(l_vis[i], 10000)
|
||||||
|
|
||||||
|
# Step 1: Default matematico LOCALE
|
||||||
|
types = {'L': loc_L, 'M': loc_M, 'H': loc_H}
|
||||||
|
dominant_type = max(types, key=types.get)
|
||||||
|
|
||||||
|
# Quantità da mostrare: Media Globale
|
||||||
|
Cl = c_avg_tot
|
||||||
|
|
||||||
|
# Step 2: Override Tattico LOCALE
|
||||||
|
# Se LOCALMENTE le basse sono > 40%, vincono loro.
|
||||||
|
# (Soglia abbassata a 40 per catturare il 51%)
|
||||||
|
if loc_L > 40:
|
||||||
|
dominant_type = 'L'
|
||||||
|
# Se localmente è nuvoloso basso, forziamo la copertura visiva alta
|
||||||
|
# anche se la media globale è più bassa
|
||||||
|
if Cl < loc_L: Cl = int(loc_L)
|
||||||
|
|
||||||
|
# Step 3: Nebbia (F)
|
||||||
|
is_fog = False
|
||||||
|
if Vis < 2000 or Code in [45, 48]:
|
||||||
|
is_fog = True
|
||||||
|
elif Rh >= 96 and loc_L > 40:
|
||||||
|
is_fog = True
|
||||||
|
|
||||||
|
if is_fog:
|
||||||
|
dominant_type = 'F'
|
||||||
|
if Cl < 100: Cl = 100
|
||||||
|
|
||||||
|
# Check varianza spaziale
|
||||||
|
min_p = min(points_cl_tot[i])
|
||||||
|
max_p = max(points_cl_tot[i])
|
||||||
|
var_symbol = ""
|
||||||
|
if (max_p - min_p) > 20:
|
||||||
|
var_symbol = "~"
|
||||||
|
|
||||||
|
cl_str = f"{var_symbol}{Cl}{dominant_type}"
|
||||||
|
|
||||||
|
UV = get_val(l_uv[i], 0)
|
||||||
|
uv_suffix = ""
|
||||||
|
if UV >= 10: uv_suffix = "E"
|
||||||
|
elif UV >= 7: uv_suffix = "H"
|
||||||
|
|
||||||
|
Wspd = get_val(l_wspd[i], 0)
|
||||||
|
Gust = get_val(l_gust[i], 0)
|
||||||
|
Wdir = int(get_val(l_wdir[i], 0))
|
||||||
|
Cape = get_val(l_cape[i], 0)
|
||||||
|
IsDay = int(l_day[i]) if l_day[i] is not None else 1
|
||||||
|
|
||||||
|
card = degrees_to_cardinal(Wdir)
|
||||||
|
w_val = Gust if (Gust - Wspd) > 15 else Wspd
|
||||||
|
w_txt = f"{card} {int(round(w_val))}"
|
||||||
|
if (Gust - Wspd) > 15:
|
||||||
|
g_txt = f"G{int(round(w_val))}"
|
||||||
|
if len(card)+len(g_txt) <= 5: w_txt = f"{card}{g_txt}"
|
||||||
|
elif len(card)+1+len(g_txt) <= 5: w_txt = f"{card} {g_txt}"
|
||||||
|
else: w_txt = g_txt
|
||||||
|
w_fmt = f"{w_txt:<5}"
|
||||||
|
|
||||||
|
sky, sgx = get_icon_set(Pr, Sn, Code, IsDay, Cl, Vis, T, get_val(l_rain[i], 0), Gust, Cape, dominant_type)
|
||||||
|
sky_fmt = f"{sky}{uv_suffix}"
|
||||||
|
|
||||||
|
lines.append(f"{dt.strftime('%H'):<2} {t_s:>4} {Rh:>3} {p_s:>3} {w_fmt} {cl_str:>5} {sky_fmt:<2} {sgx:<2}")
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Errore riga meteo {i}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if count > 0:
|
||||||
|
day_label = f"{['Lun','Mar','Mer','Gio','Ven','Sab','Dom'][now.weekday()]} {now.day}"
|
||||||
|
blocks.append(f"*{day_label} ({label})*\n```text\n" + "\n".join(lines) + "\n```")
|
||||||
|
now = end_time
|
||||||
|
|
||||||
|
return f"🌤️ *METEO REPORT*\n📍 {location_name}\n🧠 Fonte: {model_name}\n\n" + "\n\n".join(blocks)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
args_parser = argparse.ArgumentParser()
|
||||||
|
args_parser.add_argument("--query", help="Nome città")
|
||||||
|
args_parser.add_argument("--home", action="store_true", help="Usa coordinate casa")
|
||||||
|
args_parser.add_argument("--debug", action="store_true", help="Mostra i valori dei 5 punti")
|
||||||
|
args = args_parser.parse_args()
|
||||||
|
|
||||||
|
if args.home:
|
||||||
|
print(generate_weather_report(HOME_LAT, HOME_LON, HOME_NAME, args.debug, "SM"))
|
||||||
|
elif args.query:
|
||||||
|
coords = get_coordinates(args.query)
|
||||||
|
if coords:
|
||||||
|
lat, lon, name, cc = coords
|
||||||
|
print(generate_weather_report(lat, lon, name, args.debug, cc))
|
||||||
|
else:
|
||||||
|
print(f"❌ Città '{args.query}' non trovata.")
|
||||||
|
else:
|
||||||
|
print("Uso: meteo.py --query 'Nome Città' oppure --home [--debug]")
|
||||||
263
services/telegram-bot/previsione7.py
Executable file
263
services/telegram-bot/previsione7.py
Executable file
@@ -0,0 +1,263 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import requests
|
||||||
|
import argparse
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
# --- CONFIGURAZIONE DEFAULT ---
|
||||||
|
DEFAULT_LAT = 43.9356
|
||||||
|
DEFAULT_LON = 12.4296
|
||||||
|
DEFAULT_NAME = "🏠 Casa (Strada Cà Toro)"
|
||||||
|
|
||||||
|
# --- TIMEZONE ---
|
||||||
|
TZ_STR = "Europe/Rome"
|
||||||
|
|
||||||
|
# --- TELEGRAM CONFIG ---
|
||||||
|
ADMIN_CHAT_ID = "64463169"
|
||||||
|
TOKEN_FILE_HOME = os.path.expanduser("~/.telegram_dpc_bot_token")
|
||||||
|
TOKEN_FILE_ETC = "/etc/telegram_dpc_bot_token"
|
||||||
|
|
||||||
|
# --- SOGLIE ---
|
||||||
|
SOGLIA_VENTO_KMH = 40.0
|
||||||
|
MIN_MM_PER_EVENTO = 0.1
|
||||||
|
|
||||||
|
def get_bot_token():
|
||||||
|
for path in [TOKEN_FILE_ETC, TOKEN_FILE_HOME]:
|
||||||
|
if os.path.exists(path):
|
||||||
|
try:
|
||||||
|
with open(path, 'r') as f: return f.read().strip()
|
||||||
|
except: pass
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def get_coordinates(query):
|
||||||
|
if not query or query.lower() == "casa":
|
||||||
|
return DEFAULT_LAT, DEFAULT_LON, DEFAULT_NAME
|
||||||
|
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)
|
||||||
|
res = resp.json().get("results", [])[0]
|
||||||
|
return res['latitude'], res['longitude'], f"{res.get('name')} ({res.get('country_code','')})"
|
||||||
|
except: return None, None, None
|
||||||
|
|
||||||
|
def get_weather(lat, lon):
|
||||||
|
url = "https://api.open-meteo.com/v1/forecast"
|
||||||
|
params = {
|
||||||
|
"latitude": lat, "longitude": lon,
|
||||||
|
"hourly": "temperature_2m,precipitation_probability,precipitation,weathercode,windspeed_10m,dewpoint_2m",
|
||||||
|
"daily": "temperature_2m_max,temperature_2m_min,sunrise,sunset",
|
||||||
|
"timezone": TZ_STR, "models": "best_match", "forecast_days": 8
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, params=params, timeout=10)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
except: return None
|
||||||
|
|
||||||
|
def get_precip_type(code):
|
||||||
|
"""Definisce il tipo di precipitazione in base al codice WMO."""
|
||||||
|
# Neve (71-77, 85-86)
|
||||||
|
if (71 <= code <= 77) or code in [85, 86]: return "❄️ Neve"
|
||||||
|
# Grandine (96-99)
|
||||||
|
if code in [96, 99]: return "⚡🌨 Grandine"
|
||||||
|
# Pioggia congelantesi (66-67)
|
||||||
|
if code in [66, 67]: return "🧊☔ Pioggia Congelantesi"
|
||||||
|
# Pioggia standard
|
||||||
|
return "☔ Pioggia"
|
||||||
|
|
||||||
|
def get_intensity_label(mm_h):
|
||||||
|
if mm_h < 2.5: return "Debole"
|
||||||
|
if mm_h < 7.6: return "Moderata"
|
||||||
|
return "Forte ⚠️"
|
||||||
|
|
||||||
|
def analyze_daily_events(times, codes, probs, precip, winds, temps, dewpoints):
|
||||||
|
"""Scansiona le 24 ore e trova blocchi di eventi continui."""
|
||||||
|
events = []
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 1. LIVELLO PERICOLI (Ghiaccio, Gelo, Brina)
|
||||||
|
# ==========================================
|
||||||
|
in_ice = False
|
||||||
|
start_ice = 0
|
||||||
|
ice_type = ""
|
||||||
|
|
||||||
|
for i in range(len(times)):
|
||||||
|
t = temps[i]
|
||||||
|
d = dewpoints[i]
|
||||||
|
p = precip[i]
|
||||||
|
c = codes[i]
|
||||||
|
|
||||||
|
current_ice_condition = None
|
||||||
|
|
||||||
|
# A. GELICIDIO (Pericolo massimo)
|
||||||
|
# Se il codice è esplicitamente Gelicidio (66,67) OPPURE piove (codici pioggia) con T < 0
|
||||||
|
is_raining_code = (50 <= c <= 69) or (80 <= c <= 82)
|
||||||
|
if c in [66, 67] or (p > 0 and t <= 0 and is_raining_code):
|
||||||
|
current_ice_condition = "🧊☠️ GELICIDIO"
|
||||||
|
|
||||||
|
# B. GHIACCIO/BRINA (Strada Scivolosa)
|
||||||
|
# Niente precipitazioni, T bassa (<2°C) e DewPoint vicinissimo alla T (<1°C diff)
|
||||||
|
elif p == 0 and t <= 2.0 and (t - d) < 1.0:
|
||||||
|
current_ice_condition = "⛸️⚠️ GHIACCIO/BRINA (Strada Scivolosa)"
|
||||||
|
|
||||||
|
# C. GELATA SEMPLICE (T < 0)
|
||||||
|
elif t < 0:
|
||||||
|
current_ice_condition = "❄️ Gelata notturna"
|
||||||
|
|
||||||
|
# Logica raggruppamento
|
||||||
|
if current_ice_condition and not in_ice:
|
||||||
|
in_ice = True
|
||||||
|
start_ice = i
|
||||||
|
ice_type = current_ice_condition
|
||||||
|
elif (not current_ice_condition and in_ice) or (in_ice and current_ice_condition != ice_type) or (in_ice and i == len(times)-1):
|
||||||
|
end_idx = i if not current_ice_condition else i
|
||||||
|
if end_idx > start_ice:
|
||||||
|
start_time = times[start_ice].split("T")[1][:5]
|
||||||
|
end_time = times[end_idx].split("T")[1][:5]
|
||||||
|
min_t_block = min(temps[start_ice:end_idx+1])
|
||||||
|
events.append(f"{ice_type}: {start_time}-{end_time} (Min: {min_t_block}°C)")
|
||||||
|
|
||||||
|
in_ice = False
|
||||||
|
if current_ice_condition:
|
||||||
|
in_ice = True
|
||||||
|
start_ice = i
|
||||||
|
ice_type = current_ice_condition
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 2. LIVELLO PRECIPITAZIONI (Pioggia, Neve)
|
||||||
|
# ==========================================
|
||||||
|
# Nota: Non sopprimiamo più nulla. Se nevica mentre gela, li segnaliamo entrambi.
|
||||||
|
in_rain = False
|
||||||
|
start_idx = 0
|
||||||
|
current_rain_type = ""
|
||||||
|
|
||||||
|
for i in range(len(times)):
|
||||||
|
is_raining = precip[i] >= MIN_MM_PER_EVENTO
|
||||||
|
|
||||||
|
if is_raining and not in_rain:
|
||||||
|
in_rain = True
|
||||||
|
start_idx = i
|
||||||
|
current_rain_type = get_precip_type(codes[i])
|
||||||
|
|
||||||
|
# Cambio tipo precipitazione (es. da Pioggia a Neve nello stesso blocco)
|
||||||
|
elif in_rain and is_raining and get_precip_type(codes[i]) != current_rain_type:
|
||||||
|
# Chiudiamo il blocco precedente e ne apriamo uno nuovo
|
||||||
|
end_idx = i
|
||||||
|
block_precip = precip[start_idx:end_idx]
|
||||||
|
tot_mm = sum(block_precip)
|
||||||
|
max_prob = max(probs[start_idx:end_idx])
|
||||||
|
start_time = times[start_idx].split("T")[1][:5]
|
||||||
|
end_time = times[end_idx].split("T")[1][:5] # Qui combacia
|
||||||
|
avg_intensity = tot_mm / len(block_precip)
|
||||||
|
|
||||||
|
events.append(
|
||||||
|
f"{current_rain_type} ({get_intensity_label(avg_intensity)}):\n"
|
||||||
|
f" 🕒 {start_time}-{end_time} | 💧 {tot_mm:.1f}mm | Prob: {max_prob}%"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Riavvia nuovo tipo
|
||||||
|
start_idx = i
|
||||||
|
current_rain_type = get_precip_type(codes[i])
|
||||||
|
|
||||||
|
elif (not is_raining and in_rain) or (in_rain and i == len(times)-1):
|
||||||
|
in_rain = False
|
||||||
|
end_idx = i if not is_raining else i + 1
|
||||||
|
|
||||||
|
block_precip = precip[start_idx:end_idx]
|
||||||
|
tot_mm = sum(block_precip)
|
||||||
|
|
||||||
|
if tot_mm > 0:
|
||||||
|
max_prob = max(probs[start_idx:end_idx])
|
||||||
|
start_time = times[start_idx].split("T")[1][:5]
|
||||||
|
end_time = times[end_idx-1].split("T")[1][:5]
|
||||||
|
avg_intensity = tot_mm / len(block_precip)
|
||||||
|
|
||||||
|
events.append(
|
||||||
|
f"{current_rain_type} ({get_intensity_label(avg_intensity)}):\n"
|
||||||
|
f" 🕒 {start_time}-{end_time} | 💧 {tot_mm:.1f}mm | Prob: {max_prob}%"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 3. LIVELLO VENTO
|
||||||
|
# ==========================================
|
||||||
|
max_wind = max(winds)
|
||||||
|
if max_wind > SOGLIA_VENTO_KMH:
|
||||||
|
peak_idx = winds.index(max_wind)
|
||||||
|
peak_time = times[peak_idx].split("T")[1][:5]
|
||||||
|
events.append(f"💨 Vento Forte: Picco {max_wind}km/h alle {peak_time}")
|
||||||
|
|
||||||
|
return events
|
||||||
|
|
||||||
|
def format_report(data, location_name):
|
||||||
|
hourly = data['hourly']
|
||||||
|
daily = data['daily']
|
||||||
|
|
||||||
|
msg = f"🌍 <b>METEO ALERT: {location_name.upper()}</b>\n"
|
||||||
|
msg += f"📡 <i>Modelli: AROME/ICON HD</i>\n\n"
|
||||||
|
|
||||||
|
daily_map = defaultdict(list)
|
||||||
|
for i, t in enumerate(hourly['time']):
|
||||||
|
daily_map[t.split("T")[0]].append(i)
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for day_date, indices in daily_map.items():
|
||||||
|
if count >= 7: break
|
||||||
|
|
||||||
|
d_times = [hourly['time'][i] for i in indices]
|
||||||
|
d_codes = [hourly['weathercode'][i] for i in indices]
|
||||||
|
d_probs = [hourly['precipitation_probability'][i] for i in indices]
|
||||||
|
d_precip = [hourly['precipitation'][i] for i in indices]
|
||||||
|
d_winds = [hourly['windspeed_10m'][i] for i in indices]
|
||||||
|
d_temps = [hourly['temperature_2m'][i] for i in indices]
|
||||||
|
d_dews = [hourly['dewpoint_2m'][i] for i in indices]
|
||||||
|
|
||||||
|
try:
|
||||||
|
t_min = daily['temperature_2m_min'][count]
|
||||||
|
t_max = daily['temperature_2m_max'][count]
|
||||||
|
except:
|
||||||
|
t_min, t_max = min(d_temps), max(d_temps)
|
||||||
|
|
||||||
|
events_list = analyze_daily_events(d_times, d_codes, d_probs, d_precip, d_winds, d_temps, d_dews)
|
||||||
|
|
||||||
|
dt = datetime.datetime.strptime(day_date, "%Y-%m-%d")
|
||||||
|
day_str = dt.strftime("%a %d/%m")
|
||||||
|
|
||||||
|
msg += f"📅 <b>{day_str}</b> 🌡️ {t_min:.0f}°/{t_max:.0f}°C\n"
|
||||||
|
|
||||||
|
if events_list:
|
||||||
|
for ev in events_list:
|
||||||
|
msg += f" ➤ {ev}\n"
|
||||||
|
else:
|
||||||
|
msg += " ✅ <i>Nessun fenomeno rilevante</i>\n"
|
||||||
|
|
||||||
|
msg += "\n"
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def send_telegram(text, chat_id, token):
|
||||||
|
requests.post(f"https://api.telegram.org/bot{token}/sendMessage",
|
||||||
|
json={"chat_id": chat_id, "text": text, "parse_mode": "HTML"})
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("query", nargs="?", default="casa")
|
||||||
|
parser.add_argument("--chat_id")
|
||||||
|
parser.add_argument("--debug", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
token = get_bot_token()
|
||||||
|
dest_chat = args.chat_id if args.chat_id and not args.debug else ADMIN_CHAT_ID
|
||||||
|
|
||||||
|
lat, lon, name = get_coordinates(args.query)
|
||||||
|
if not lat: return send_telegram(f"❌ '{args.query}' non trovato.", dest_chat, token)
|
||||||
|
|
||||||
|
data = get_weather(lat, lon)
|
||||||
|
if not data: return send_telegram("❌ Errore dati meteo.", dest_chat, token)
|
||||||
|
|
||||||
|
send_telegram(format_report(data, name), dest_chat, token)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user