From c35147188b12a12734f47d48e18d6cf6183c47f7 Mon Sep 17 00:00:00 2001 From: juanjo Date: Sat, 5 Sep 2026 19:00:52 +0200 Subject: [PATCH] 05-sep: [parental] pausar y bloquear dispositivos por nombre --- vigilancia/asus_api_control.py | 36 ++++++++++++++++++++ vigilancia/pi_bot.py | 62 ++++++++++++++++++++++++---------- 2 files changed, 80 insertions(+), 18 deletions(-) diff --git a/vigilancia/asus_api_control.py b/vigilancia/asus_api_control.py index 26799ed..ce72057 100644 --- a/vigilancia/asus_api_control.py +++ b/vigilancia/asus_api_control.py @@ -7,6 +7,7 @@ import unicodedata import aiohttp from asusrouter import AsusData, AsusRouter +from asusrouter.modules.parental_control import PCRuleType, ParentalControlRule def _load_env(): @@ -71,6 +72,41 @@ async def _parental_data(): 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, + 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 cambiar_bloqueo(nombre, blocked): + device = resolver_dispositivo(nombre) + if not device: + 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']}" diff --git a/vigilancia/pi_bot.py b/vigilancia/pi_bot.py index 4e9ab35..c813611 100644 --- a/vigilancia/pi_bot.py +++ b/vigilancia/pi_bot.py @@ -15,6 +15,7 @@ ultimos_torrents = {} # chat_id -> {'titulo':..., 'torrents':[...]} flujo_genero = {} # chat_id -> 'esperando_tipo' | 'esperando_genero' | None flujo_genero_tipo = {} # chat_id -> 'movie' | 'series' (tipo ya elegido) panel_descargas = {} # chat_id -> estado del panel editable +temporizadores_parental = {} # alias normalizado -> Timer de desbloqueo BASE = os.path.dirname(os.path.abspath(__file__)) LOG = os.path.join(BASE, "bot.log") @@ -458,23 +459,48 @@ def procesar(texto, chat_id): else: enviar("AcciΓ³n de router no reconocida. Usa /router ayuda.", chat_id) elif comando == "parental": + partes_parental = argumento.split() + accion_parental = partes_parental[0].lower() if partes_parental else "listar" try: - from router_control import control_parental - enviar( - "πŸ‘ͺ RUTINAS PARENTALES\n" - "Alias reconocidos: InΓ©s, INES, Carolina.\n" - "Rutinas previstas:\n" - "/parental listar\n" - "/parental activar <rutina>\n" - "/parental desactivar <rutina>\n" - "/parental pausa <rutina> <minutos>\n\n" - "Las acciones por nombre quedan preparadas para la primera rutina.\n\n" - "Estado actual:\n" + control_parental(), - chat_id, - ) + from asus_api_control import cambiar_bloqueo, normalizar_nombre + if accion_parental == "listar": + from router_control import control_parental + enviar("πŸ‘ͺ CONTROL PARENTAL ASUS\n" + control_parental(), chat_id) + elif accion_parental in ("desactivar", "bloquear") and len(partes_parental) >= 2: + alias = " ".join(partes_parental[1:]) + enviar(f"πŸ”’ {cambiar_bloqueo(alias, True)}", chat_id) + elif accion_parental in ("activar", "desbloquear") and len(partes_parental) >= 2: + alias = " ".join(partes_parental[1:]) + timer = temporizadores_parental.pop(normalizar_nombre(alias), None) + if timer: + timer.cancel() + enviar(f"πŸ”“ {cambiar_bloqueo(alias, False)}", chat_id) + elif accion_parental == "pausa" and len(partes_parental) >= 3: + alias = " ".join(partes_parental[1:-1]) + minutos = int(partes_parental[-1]) + if minutos < 1 or minutos > 1440: + raise ValueError("Los minutos deben estar entre 1 y 1440") + cambiar_bloqueo(alias, True) + clave = normalizar_nombre(alias) + anterior = temporizadores_parental.pop(clave, None) + if anterior: + anterior.cancel() + timer = threading.Timer( + minutos * 60, + lambda: cambiar_bloqueo(alias, False), + ) + timer.daemon = True + temporizadores_parental[clave] = timer + timer.start() + enviar(f"⏸️ Bloqueado {alias} durante {minutos} minutos.", chat_id) + else: + enviar( + "Uso: /parental pausa InΓ©s 60, " + "/parental desactivar Carolina o " + "/parental activar InΓ©s.", chat_id) except Exception as e: log(f"parental error: {e}") - enviar(f"❌ Error consultando parental: {e}", chat_id) + enviar(f"❌ Error en control parental: {e}", chat_id) elif t in ("pelis", "/pelis"): procesar_pelis(chat_id) elif t in ("series", "/series"): @@ -532,10 +558,10 @@ def procesar(texto, chat_id): "━━━ πŸ‘ͺ CONTROL PARENTAL ━━━\n" "14. parental listar β€” estado y dispositivos\n" " ✏️ Ej: /parental listar\n" - "15. parental activar|desactivar β€” rutinas por nombre (preparado)\n" - " ✏️ Ej: /parental activar noches_escolares\n" - "16. parental pausa β€” pausa temporal (preparado)\n" - " ✏️ Ej: /parental pausa noches_escolares 60\n\n" + "15. parental desactivar|activar β€” bloquea o desbloquea\n" + " ✏️ Ej: /parental desactivar InΓ©s\n" + "16. parental pausa β€” bloquea X minutos\n" + " ✏️ Ej: /parental pausa Carolina 60\n\n" "━━━ πŸ› οΈ SISTEMA ━━━\n" "17. estado β€” estado de la Pi\n" " ✏️ Ej: /estado\n"