"""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 "
Sin datos AiMesh
" 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"
{html.escape(texto)}
" 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..."