05-sep: [vigilancia] comandos /aiMesh /qos /limite /seguridad para el ASUS desde Telegram

This commit is contained in:
juanjo
2026-09-05 19:38:03 +02:00
parent e15204a5e8
commit 15f19a7900
7 changed files with 987 additions and 10 deletions

View File

@@ -1,6 +1,7 @@
"""Lectura segura de datos ASUSWRT mediante la API web del router."""
import asyncio
import html
import json
import os
import unicodedata
@@ -8,6 +9,7 @@ import unicodedata
import aiohttp
from asusrouter import AsusData, AsusRouter
from asusrouter.modules.parental_control import PCRuleType, ParentalControlRule
from asusrouter.modules.system import AsusSystem
def _load_env():
@@ -110,3 +112,115 @@ def cambiar_bloqueo(nombre, blocked):
raise ValueError(f"Dispositivo no reconocido: {nombre}")
asyncio.run(_set_block(device["mac"], device["name"], blocked))
return f"{'Bloqueado' if blocked else 'Activado'}: {device['name']}"
async def _aimesh_data():
username = _env("ASUS_USERNAME")
password = _env("ASUS_PASSWORD")
if not username or not password:
raise RuntimeError(
"Falta ASUS_USERNAME o ASUS_PASSWORD en el .env de la Raspberry"
)
session = aiohttp.ClientSession()
router = AsusRouter(
hostname=_env("ASUS_ROUTER_IP", "192.168.50.1"),
username=username,
password=password,
session=session,
)
try:
await router.async_connect()
return await router.async_get_data(AsusData.AIMESH, force=True)
finally:
await router.async_disconnect()
await session.close()
async def _aimesh_action(cmd, mac=None):
username = _env("ASUS_USERNAME")
password = _env("ASUS_PASSWORD")
if not username or not password:
raise RuntimeError(
"Falta ASUS_USERNAME o ASUS_PASSWORD en el .env de la Raspberry"
)
session = aiohttp.ClientSession()
router = AsusRouter(
hostname=_env("ASUS_ROUTER_IP", "192.168.50.1"),
username=username,
password=password,
session=session,
)
try:
await router.async_connect()
if cmd == "reconstruir":
return await router.async_set_state(AsusSystem.AIMESH_REBUILD)
if cmd == "reiniciar_todo":
return await router.async_set_state(AsusSystem.AIMESH_REBOOT)
return await router.async_set_state(
AsusSystem.NODE_REBOOT, device_list=mac
)
finally:
await router.async_disconnect()
await session.close()
def _nodo_texto(node):
tipo = "ROUTER" if getattr(node, "type", "") == "router" else "NODO"
alias = node.alias or node.model or "Sin nombre"
lineas = [f"{tipo} · {alias} ({node.model})"]
estado = "conectado" if node.status else "apagado"
lineas.append(f" estado: {estado}")
if node.ip:
lineas.append(f" ip: {node.ip}")
if node.fw:
lineas.append(f" firmware: {node.fw}")
if node.mac:
lineas.append(f" mac: {node.mac}")
parent = node.parent or {}
if parent:
rssi = parent.get("rssi", "?")
conexion = parent.get("connection", "?")
lineas.append(f" enlace: {conexion} · rssi={rssi}")
for banda, ssid in (node.ap or {}).items():
lineas.append(f" wifi {banda}: {ssid}")
return "\n".join(lineas)
def aimesh_estado():
data = asyncio.run(_aimesh_data()) or {}
if not data:
return "<pre>Sin datos AiMesh</pre>"
nodos = list(data.values())
nodos.sort(key=lambda n: getattr(n, "level", 0))
texto = "\n\n".join(_nodo_texto(n) for n in nodos)
return f"<pre>{html.escape(texto)}</pre>"
def _resolver_nodo(nombre):
data = asyncio.run(_aimesh_data()) or {}
clave = normalizar_nombre(nombre)
for mac, node in data.items():
if mac and mac.lower() == nombre.lower().strip():
return node
alias = normalizar_nombre(node.alias or "")
if clave and (alias == clave or clave in alias or alias in clave):
return node
return None
def aimesh_reiniciar_nodo(nombre):
node = _resolver_nodo(nombre)
if not node:
raise ValueError(f"Nodo AiMesh no encontrado: {nombre}")
asyncio.run(_aimesh_action("nodo", mac=node.mac))
return f"🔄 Nodo reiniciado: {node.alias or node.model} ({node.mac})"
def aimesh_reconstruir():
asyncio.run(_aimesh_action("reconstruir"))
return "🛠️ AiMesh reconstruida. Los nodos se reconectarán en unos minutos."
def aimesh_reiniciar_todo():
asyncio.run(_aimesh_action("reiniciar_todo"))
return "⚠️ Router y todos los nodos se están reiniciando..."

View File

@@ -497,10 +497,115 @@ def procesar(texto, chat_id):
enviar(
"Uso: <code>/parental pausa Inés 60</code>, "
"<code>/parental desactivar Carolina</code> o "
"<code>/parental activar Inés</code>.", chat_id)
"<code>/parental activar Inés</code>.", chat_id)
except Exception as e:
log(f"parental error: {e}")
enviar(f"❌ Error en control parental: {e}", chat_id)
elif comando == "aiMesh":
partes_mesh = argumento.split()
accion_mesh = partes_mesh[0].lower() if partes_mesh else "estado"
try:
from asus_api_control import (
aimesh_estado,
aimesh_reconstruir,
aimesh_reiniciar_nodo,
)
if accion_mesh in ("estado", "nodos", "listar"):
enviar("🌐 <b>AIMESH ASUS</b>\n" + aimesh_estado(), chat_id)
elif accion_mesh == "reiniciar" and len(partes_mesh) >= 2:
if partes_mesh[-1] != "confirmar":
enviar("Para reiniciar un nodo responde: <code>/aiMesh reiniciar &lt;nodo&gt; confirmar</code>", chat_id)
else:
nodo = " ".join(partes_mesh[1:-1])
enviar(f"🔄 Reiniciando el nodo <b>{nodo}</b>...", chat_id)
enviar(aimesh_reiniciar_nodo(nodo), chat_id)
elif accion_mesh == "reconstruir":
if len(partes_mesh) < 2 or partes_mesh[-1] != "confirmar":
enviar("Para reconstruir la malla responde: <code>/aiMesh reconstruir confirmar</code>", chat_id)
else:
enviar("⚠️ Reconstruyendo la malla AiMesh...", chat_id)
enviar(aimesh_reconstruir(), chat_id)
else:
enviar(
"Uso:\n<code>/aiMesh estado</code> — nodos y señal\n"
"<code>/aiMesh reiniciar &lt;nodo&gt; confirmar</code>\n"
"<code>/aiMesh reconstruir confirmar</code>", chat_id)
except Exception as e:
log(f"aiMesh error: {e}")
enviar(f"❌ Error en AiMesh: {e}", chat_id)
elif comando == "qos":
partes_qos = argumento.lower().split()
accion_qos = partes_qos[0] if partes_qos else "estado"
try:
from router_control import qos_apagar, qos_estado, qos_set
if accion_qos == "estado":
enviar("⚙️ <b>QOS ASUS</b>\n" + qos_estado(), chat_id)
elif accion_qos in ("adaptive", "tradicional") and len(partes_qos) >= 2 and partes_qos[-1] == "confirmar":
enviar("⏳ Aplicando QoS...", chat_id)
enviar(qos_set(accion_qos), chat_id)
elif accion_qos == "apagar" and len(partes_qos) >= 2 and partes_qos[-1] == "confirmar":
enviar("⏳ Apagando QoS...", chat_id)
enviar(qos_apagar(), chat_id)
else:
enviar(
"Uso:\n<code>/qos estado</code>\n"
"<code>/qos adaptive confirmar</code> — auto gaming/streaming\n"
"<code>/qos tradicional confirmar</code> — prioridades manuales\n"
"<code>/qos apagar confirmar</code>", chat_id)
except Exception as e:
log(f"qos error: {e}")
enviar(f"❌ Error en QoS: {e}", chat_id)
elif comando == "limite":
partes_lim = argumento.split()
accion_lim = partes_lim[0].lower() if partes_lim else "listar"
try:
from router_control import limite_listar, limite_poner, limite_quitar
if accion_lim == "listar":
enviar("🚦 <b>LÍMITES DE VELOCIDAD</b>\n" + limite_listar(), chat_id)
elif accion_lim in ("poner", "set") and len(partes_lim) >= 4:
try:
dl = float(partes_lim[-2])
ul = float(partes_lim[-1])
except ValueError:
enviar("Las velocidades deben ser números en Mbps.", chat_id)
else:
dispositivo = " ".join(partes_lim[1:-2]) or partes_lim[1]
enviar(limite_poner(dispositivo, dl, ul), chat_id)
elif accion_lim in ("quitar", "del", "remove") and len(partes_lim) >= 2:
dispositivo = " ".join(partes_lim[1:])
enviar(limite_quitar(dispositivo), chat_id)
else:
enviar(
"Uso:\n<code>/limite listar</code>\n"
"<code>/limite poner &lt;dispositivo&gt; &lt;bajada&gt; &lt;subida&gt;</code> (Mbps)\n"
" ✏️ Ej: <code>/limite poner NAS 50 20</code>\n"
"<code>/limite quitar &lt;dispositivo&gt;</code>", chat_id)
except Exception as e:
log(f"limite error: {e}")
enviar(f"❌ Error en límites: {e}", chat_id)
elif comando == "seguridad":
partes_seg = argumento.lower().split()
accion_seg = partes_seg[0] if partes_seg else "estado"
try:
from router_control import (
seguridad_estado,
seguridad_keyword,
)
if accion_seg == "estado":
enviar("🛡️ <b>SEGURIDAD ASUS</b>\n" + seguridad_estado(), chat_id)
elif accion_seg in ("bloquear", "desbloquear") and len(partes_seg) >= 2:
palabra = " ".join(partes_seg[1:])
activar = accion_seg == "bloquear"
enviar(seguridad_keyword(palabra, activar), chat_id)
else:
enviar(
"Uso (filtro de contenidos AiProtection):\n"
"<code>/seguridad estado</code>\n"
"<code>/seguridad bloquear &lt;palabra&gt;</code> — p. ej. tiktok\n"
"<code>/seguridad desbloquear &lt;palabra&gt;</code>", chat_id)
except Exception as e:
log(f"seguridad error: {e}")
enviar(f"❌ Error en seguridad: {e}", chat_id)
elif t in ("pelis", "/pelis"):
procesar_pelis(chat_id)
elif t in ("series", "/series"):
@@ -579,15 +684,15 @@ def procesar(texto, chat_id):
" ✏️ Ej: <code>/reboot</code> → confirmar con <code>/confirmar-reboot</code>\n"
"<b>24.</b> <code>ayuda</code> — este índice\n"
" ✏️ Ej: <code>/ayuda</code>\n\n"
"━━━ 🆕 ROUTER ASUS — PRÓXIMAMENTE ━━━\n"
"<b>25.</b> <code>qos</code> — prioridad de ancho de banda por tipo de tráfico\n"
" ✏️ Ej: <code>/qos modo adaptive</code> — gaming/streaming con prioridad\n"
"<b>26.</b> <code>aiMesh</code> — red en malla con tus 2 módulos ASUS\n"
" ✏️ Ej: <code>/aiMesh estado</code> — nodos, señal y roaming unificado\n"
"<b>27.</b> <code>seguridad</code> — AiProtection: bloqueo de webs por categoría\n"
" ✏️ Ej: <code>/seguridad bloquear Inés redes sociales</code>\n"
"<b>28.</b> <code>limite</code> — límite de velocidad por dispositivo\n"
" ✏️ Ej: <code>/limite poner NAS 50 20</code> — 50/20 Mbps",
"━━━ 🌐 ROUTER ASUS — MESH / QoS / SEGURIDAD ━━━\n"
"<b>25.</b> <code>qos estado</code> — ancho de banda con prioridades\n"
" ✏️ Ej: <code>/qos adaptive confirmar</code> — auto gaming/streaming\n"
"<b>26.</b> <code>aiMesh estado</code> — nodos, señal y roaming unificado\n"
" ✏️ Ej: <code>/aiMesh reiniciar nodo confirmar</code>\n"
"<b>27.</b> <code>seguridad estado</code> — AiProtection: filtro de webs\n"
" ✏️ Ej: <code>/seguridad bloquear tiktok</code>\n"
"<b>28.</b> <code>limite listar</code> — límite de velocidad por dispositivo\n"
" ✏️ Ej: <code>/limite poner NAS 50 20</code> — 50/20 Mbps",
chat_id)
elif t in ("servicios", "/servicios"):
estado_svc = sh("systemctl is-active pi-bot tv-api autossh-tunel")

View File

@@ -2,3 +2,5 @@ opencv-python
requests
python-dotenv
ultralytics
asusrouter==1.21.3
aiohttp

View File

@@ -143,6 +143,245 @@ def reiniciar():
return _run("reboot")
QOS_TIPOS = {
"0": "Traditional (por prioridades)",
"1": "Adaptive (auto gaming/streaming)",
"2": "Bandwidth limiter",
"3": "GeForce Now",
}
def _set_nvram(entradas, servicio=None):
partes = [
f"nvram set {clave}={shlex.quote(str(valor))}"
for clave, valor in entradas
]
partes.append("nvram commit")
if servicio:
partes.append(f"service {servicio}")
return _run("; ".join(partes))
def _nombre_por_mac(mac):
_devices, _by_mac = _inventario()
for ip, m, nombre in _devices:
if m == mac.upper():
return nombre
try:
from asus_api_control import DEVICE_ALIASES
for _alias, info in DEVICE_ALIASES.items():
if info["mac"].upper() == mac.upper():
return info["name"]
except Exception:
pass
return f"unknown-{mac}"
def _resolver_mac(dispositivo):
valor = str(dispositivo or "").strip()
if valor.lower().count(":") == 5:
return valor.upper()
try:
from asus_api_control import normalizar_nombre, resolver_dispositivo
except Exception:
raise ValueError(f"Dispositivo no reconocido: {dispositivo}")
info = resolver_dispositivo(valor)
if info:
return info["mac"].upper()
_devices, _by_mac = _inventario()
clave = normalizar_nombre(valor)
for ip, m, nombre in _devices:
n = normalizar_nombre(nombre)
if clave and (n == clave or clave in n or n in clave):
return m
raise ValueError(
f"Dispositivo no reconocido: {dispositivo}. Usa MAC o un alias."
)
def qos_estado():
enable = (_run("nvram get qos_enable") or "").strip()
tipo = (_run("nvram get qos_type") or "").strip()
activado = "sí" if enable == "1" else "no"
modo = QOS_TIPOS.get(tipo, f"desconocido ({tipo})")
texto = f"Activado: {activado}\nModo: {modo}"
return f"<pre>{html.escape(texto)}</pre>"
def qos_set(modo):
conf = {
"adaptive": ("1", "1"),
"tradicional": ("0", "0"),
}
if modo not in conf:
raise ValueError("Modo no válido: usa adaptive o tradicional")
qos_enable, qos_type = conf[modo]
resultado = _set_nvram(
[("qos_enable", qos_enable), ("qos_type", qos_type)], "restart_qos"
)
if resultado.startswith("Error "):
return resultado
nombre = QOS_TIPOS[qos_type]
return (
f"QoS activado en modo <b>{nombre}</b>. "
"Ten en cuenta que QoS desactiva la aceleración NAT."
)
def qos_apagar():
resultado = _set_nvram([("qos_enable", "0")], "restart_qos")
if resultado.startswith("Error "):
return resultado
return "QoS desactivado. Aceleración NAT restaurada."
def _limite_parse():
raw = _run("nvram get qos_bw_rulelist") or ""
reglas = []
for parte in str(raw).split("<"):
parte = parte.strip()
if not parte:
continue
campos = parte.rstrip(">").split(">")
if len(campos) < 4:
continue
en, mac, dl, ul = campos[:4]
if str(mac).count(":") != 5:
continue
reglas.append({
"enable": en,
"mac": mac.upper(),
"dl": dl,
"ul": ul,
})
return raw, reglas
def _limite_payload(reglas):
partes = [
f"{r['enable']}>{r['mac']}>{r['dl']}>{r['ul']}>{idx}"
for idx, r in enumerate(reglas)
]
return "<".join(partes)
def _limite_guardar(reglas):
payload = _limite_payload(reglas)
if reglas:
entradas = [
("qos_enable", "1"),
("qos_type", "2"),
("qos_bw_rulelist", payload),
]
else:
entradas = [
("qos_enable", "0"),
("qos_bw_rulelist", payload),
]
return _set_nvram(entradas, "restart_qos")
def limite_listar():
_raw, reglas = _limite_parse()
if not reglas:
return "<pre>No hay reglas de límite activas.</pre>"
lineas = ["ACTIVADO DISPOSITIVO/MAC BAJADA SUBIDA"]
for r in reglas:
nombre = _nombre_por_mac(r["mac"])
dl = f"{int(r['dl']) / 1024:.0f}" if r["dl"].isdigit() else r["dl"]
ul = f"{int(r['ul']) / 1024:.0f}" if r["ul"].isdigit() else r["ul"]
lineas.append(
f"{'sí' if r['enable'] == '1' else 'no':<9} "
f"{nombre:<20} {dl:>7} {ul:>7} Mbps"
)
return f"<pre>{html.escape(chr(10).join(lineas))}</pre>"
def limite_poner(dispositivo, dl_mbps, ul_mbps):
mac = _resolver_mac(dispositivo)
if dl_mbps <= 0 or ul_mbps <= 0:
raise ValueError("Las velocidades deben ser mayores que 0")
_raw, reglas = _limite_parse()
reglas = [r for r in reglas if r["mac"] != mac]
reglas.append({
"enable": "1",
"mac": mac,
"dl": str(int(dl_mbps * 1024)),
"ul": str(int(ul_mbps * 1024)),
})
resultado = _limite_guardar(reglas)
if resultado.startswith("Error "):
return resultado
return (
f"Límite aplicado a <b>{_nombre_por_mac(mac)}</b>: "
f"{dl_mbps:.0f} Mbps bajada / {ul_mbps:.0f} Mbps subida. "
"El bandwidth limiter desactiva la aceleración NAT."
)
def limite_quitar(dispositivo):
mac = _resolver_mac(dispositivo)
_raw, reglas = _limite_parse()
if not any(r["mac"] == mac for r in reglas):
return f"No hay límite activo para {dispositivo}."
reglas = [r for r in reglas if r["mac"] != mac]
resultado = _limite_guardar(reglas)
if resultado.startswith("Error "):
return resultado
if not reglas:
return f"Límite retirado de <b>{_nombre_por_mac(mac)}</b>. "
return f"Límite retirado de <b>{_nombre_por_mac(mac)}</b>."
def seguridad_estado():
claves = (
"bwdpi_enable",
"yadns_enable_x",
"yadns_mode",
)
lineas = []
for clave in claves:
lineas.append(f"{clave}={_run(f'nvram get {clave}')}")
keywords = _run("nvram get keyword_rulelist") or ""
palabras = [p for p in str(keywords).split("<") if p.strip()]
lineas.append(
"palabras filtradas: "
+ (", ".join(palabras) if palabras else "(ninguna)")
)
return f"<pre>{html.escape(chr(10).join(lineas))}</pre>"
def _keywords_actuales():
raw = _run("nvram get keyword_rulelist") or ""
return [p.strip().lower() for p in str(raw).split("<") if p.strip()]
def seguridad_keyword(palabra, activar):
palabra = palabra.strip().lower()
palabras = _keywords_actuales()
if activar:
if palabra in palabras:
return f"La palabra <b>{palabra}</b> ya estaba en el filtro."
palabras.append(palabra)
else:
if palabra not in palabras:
return f"La palabra <b>{palabra}</b> no estaba en el filtro."
palabras = [p for p in palabras if p != palabra]
payload = "".join(f"<{p}>" for p in palabras)
entradas = [("keyword_rulelist", payload)]
if palabras:
entradas.insert(0, ("bwdpi_enable", "1"))
resultado = _set_nvram(entradas, "restart_bwdpi")
if resultado.startswith("Error "):
return resultado
accion = "bloqueada" if activar else "desbloqueada"
if palabras:
resto = f" Filtro activo: {', '.join(palabras)}."
else:
resto = " Filtro de contenidos vacío."
return f"Palabra <b>{palabra}</b> {accion}.{resto}"
if __name__ == "__main__":
action = sys.argv[1] if len(sys.argv) > 1 else "estado"
actions = {
@@ -153,5 +392,11 @@ if __name__ == "__main__":
"reservar": reservar_dhcp,
"reservas": reservas,
"parental": control_parental,
"qos_estado": qos_estado,
"qos_adaptive": lambda: qos_set("adaptive"),
"qos_tradicional": lambda: qos_set("tradicional"),
"qos_apagar": qos_apagar,
"limite_listar": limite_listar,
"seguridad_estado": seguridad_estado,
}
print(actions.get(action, estado)())