Files
biblioteca_conocimiento_lab…/vigilancia/asus_api_control.py

325 lines
10 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 _leer_reglas():
"""Lee las reglas parentales del router como dict mac -> campos."""
from router_control import _run
def get(var):
out = _run(f"nvram get {var}")
if out.startswith("Error"):
raise RuntimeError(out)
return out
macs = [x for x in get("MULTIFILTER_MAC").split(">") if x]
names = get("MULTIFILTER_DEVICENAME").split(">")
maps = get("MULTIFILTER_MACFILTER_DAYTIME_V2").split(">")
types = get("MULTIFILTER_ENABLE").split(">")
reglas = {}
for i, mac in enumerate(macs):
reglas[mac.upper()] = {
"name": names[i] if i < len(names) else "",
"timemap": maps[i] if i < len(maps) else TIMEMAP_24_7,
"type": types[i] if i < len(types) else "2",
}
return reglas
def fijar_bloqueos(cambios):
"""Aplica varios (mac, name, blocked) con una sola lectura-modificacion-
escritura: varios bloqueos seguidos no se pisan entre si."""
from router_control import _run
import shlex
reglas = _leer_reglas()
for mac, name, blocked in cambios:
mac = (mac or "").upper()
if not mac:
continue
if blocked:
reglas[mac] = {"name": name or "", "timemap": TIMEMAP_24_7,
"type": "2"}
else:
reglas.pop(mac, None)
orden = list(reglas)
cmds = [
f"nvram set MULTIFILTER_MAC={shlex.quote('>'.join(orden))}",
"nvram set MULTIFILTER_DEVICENAME=" + shlex.quote(
">".join(reglas[m]["name"] for m in orden)),
"nvram set MULTIFILTER_MACFILTER_DAYTIME_V2=" + shlex.quote(
">".join(reglas[m]["timemap"] for m in orden)),
f"nvram set MULTIFILTER_ENABLE={shlex.quote('>'.join(reglas[m]['type'] for m in orden))}",
"nvram commit",
"service restart_firewall",
]
_run("; ".join(cmds))
return _leer_reglas()
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]}]"
try:
reglas = fijar_bloqueos([(mac, device["name"], blocked)])
except Exception as e:
return f"❌ No se pudo aplicar la regla: {e}"
dentro = mac.upper() in reglas if blocked else mac.upper() not in reglas
marca = "✅" if dentro else "❌"
base = (f"{'Bloqueado' if blocked else 'Activado'}: {etiqueta} "
f"({mac}) {marca}")
try:
from router_control import bloqueo_red
return f"{base}\n{bloqueo_red(nombre, mac, blocked)}"
except Exception as e:
return f"{base}\n⚠️ Sin verificacion de red: {e}"
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..."