Files
biblioteca_conocimiento_lab…/vigilancia/asus_api_control.py

260 lines
8.2 KiB
Python

"""Lectura segura de datos ASUSWRT mediante la API web del router."""
import asyncio
import html
import json
import os
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():
values = {}
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
try:
with open(path) as stream:
for line in stream:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key, value = line.split("=", 1)
values[key.strip()] = value.strip()
except OSError:
pass
return values
FILE_ENV = _load_env()
DEVICE_ALIASES = {
"ines": {"name": "TABLET INES", "mac": "04:34:F6:0C:8E:48"},
"tablet ines": {"name": "TABLET INES", "mac": "04:34:F6:0C:8E:48"},
"carolina": {"name": "Caroline-tablet", "mac": "3A:99:2B:ED:D3:5E"},
"caroline": {"name": "Caroline-tablet", "mac": "3A:99:2B:ED:D3:5E"},
"caroline tablet": {"name": "Caroline-tablet", "mac": "3A:99:2B:ED:D3:5E"},
}
def _env(name, default=""):
return os.getenv(name, FILE_ENV.get(name, default)).strip()
# Bloqueo 24/7 toda la semana (0=domingo..6=sabado). Sin esto la libreria
# aplica su timemap por defecto (solo miercoles/jueves por horas).
TIMEMAP_24_7 = "<".join(f"W{d}E23590000" for d in range(7))
def normalizar_nombre(nombre):
value = unicodedata.normalize("NFKD", nombre or "")
value = "".join(char for char in value if not unicodedata.combining(char))
return " ".join(value.lower().replace("-", " ").split())
def resolver_dispositivo(nombre):
return DEVICE_ALIASES.get(normalizar_nombre(nombre))
async def _parental_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.PARENTAL_CONTROL, force=True)
finally:
await router.async_disconnect()
await session.close()
async def _set_block(mac, name, blocked):
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()
rule = ParentalControlRule(
mac=mac,
name=name if blocked else None,
timemap=TIMEMAP_24_7,
type=PCRuleType.BLOCK if blocked else PCRuleType.REMOVE,
)
return await router.async_set_state(rule)
finally:
await router.async_disconnect()
await session.close()
def parental_status():
data = asyncio.run(_parental_data())
return json.dumps(data, ensure_ascii=False, indent=2, default=str)
def _mac_en_vivo(pistas):
"""Busca en los leases DHCP actuales un dispositivo cuyo hostname
contenga alguna pista. Devuelve (ip, mac, hostname) o None.
Sirve para tablets con MAC privada aleatoria (la MAC guardada rota)."""
try:
from router_control import _inventario
except Exception:
return None
try:
devices, _by_mac = _inventario()
except Exception:
return None
for ip, mac, nombre in devices:
n = normalizar_nombre(nombre)
for p in pistas:
if p and (p in n or n in p):
return ip, mac, nombre
return None
def cambiar_bloqueo(nombre, blocked):
device = resolver_dispositivo(nombre)
if not device:
raise ValueError(f"Dispositivo no reconocido: {nombre}")
mac = device["mac"]
etiqueta = device["name"]
vivo = _mac_en_vivo((normalizar_nombre(nombre),
normalizar_nombre(etiqueta)))
if vivo and vivo[1].upper() != mac.upper():
mac = vivo[1]
etiqueta = f"{etiqueta} [{vivo[2]} {vivo[0]}]"
asyncio.run(_set_block(mac, device["name"], blocked))
return f"{'Bloqueado' if blocked else 'Activado'}: {etiqueta} ({mac})"
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..."