05-sep: [dhcp] inventariar y reservar IPs del ASUS

This commit is contained in:
juanjo
2026-09-05 18:33:29 +02:00
parent bcc3671356
commit 43286045b5
3 changed files with 130 additions and 2 deletions

View File

@@ -75,6 +75,8 @@ Desde Telegram, el bot consulta el ASUS mediante la clave de servicio de la Rasp
/router estado /router estado
/router clientes /router clientes
/router wifi /router wifi
/router inventario
/router reservar confirmar
/router reiniciar confirmar /router reiniciar confirmar
``` ```
@@ -96,6 +98,58 @@ El router puede centralizar muchas funciones para Telegram:
Las acciones que cambian red, Wi-Fi, firewall o VPN deben tener confirmacion doble. Las acciones que cambian red, Wi-Fi, firewall o VPN deben tener confirmacion doble.
La reserva DHCP conserva la IP actual de cada lease y mantiene las reservas previas. Los dispositivos sin hostname quedan como `pendiente-identificar` hasta asignarles un nombre humano.
## Inventario DHCP actual del ASUS
Reservas aplicadas para los 39 leases actuales. Los nombres marcados como `pendiente-identificar` son provisionales.
| IP | MAC | Nombre actual |
|---|---|---|
| 192.168.50.2 | 1C:FE:2B:BE:2C:D4 | pendiente-identificar |
| 192.168.50.6 | D8:47:32:6E:B8:62 | termo / Tapo_SmartPlug |
| 192.168.50.8 | BE:4E:C5:AF:E3:3F | pendiente-identificar |
| 192.168.50.12 | 4C:A9:19:4E:E8:4C | pendiente-identificar |
| 192.168.50.13 | 58:11:22:1B:0E:C0 | pendiente-identificar |
| 192.168.50.16 | 14:EA:63:1D:1B:25 | Tele |
| 192.168.50.31 | 00:11:32:3B:6F:4E | MINGUEZ / NAS |
| 192.168.50.54 | 48:43:DD:73:A2:FB | pendiente-identificar |
| 192.168.50.65 | 94:B8:6D:08:2A:7D | NAHID_PC |
| 192.168.50.67 | 62:A3:89:4A:EB:70 | iPhone |
| 192.168.50.68 | AC:15:A2:89:D7:8A | P110 |
| 192.168.50.69 | DC:98:40:F0:7B:56 | HERMES2111 |
| 192.168.50.73 | 04:34:F6:0C:8E:48 | pendiente-identificar |
| 192.168.50.76 | 58:11:22:22:AD:90 | pendiente-identificar |
| 192.168.50.77 | 24:DF:A7:E0:13:C7 | pendiente-identificar |
| 192.168.50.80 | B0:CF:CB:D0:FD:35 | AmazonAQM-009R |
| 192.168.50.101 | 48:E1:E9:88:AD:FF | pendiente-identificar |
| 192.168.50.102 | 44:87:63:9B:07:A2 | 203938483430501000520018 |
| 192.168.50.104 | FC:67:1F:38:24:CD | pendiente-identificar |
| 192.168.50.108 | D4:AD:FC:1D:01:31 | pendiente-identificar |
| 192.168.50.112 | D8:1F:12:1C:FF:29 | wlan0 |
| 192.168.50.122 | 70:C9:32:1F:E5:65 | dreame_vacuum_r2205 |
| 192.168.50.127 | 10:5A:17:20:84:4F | pendiente-identificar |
| 192.168.50.137 | 74:D6:37:D8:F2:64 | amazon-a816315ca |
| 192.168.50.143 | A8:8C:3E:7C:F3:C7 | XBOX |
| 192.168.50.173 | 24:4C:E3:DF:6D:F4 | pendiente-identificar |
| 192.168.50.187 | B8:27:EB:80:C6:0E | minipc / Raspberry |
| 192.168.50.189 | D8:D6:68:BD:84:BD | lwip0 |
| 192.168.50.191 | E0:EF:BF:AA:CD:1E | pendiente-identificar |
| 192.168.50.192 | 40:A2:DB:40:C0:6E | pendiente-identificar |
| 192.168.50.203 | 70:B1:3D:A6:D2:40 | Samsung |
| 192.168.50.207 | 88:1E:5A:37:E5:52 | JUANJER |
| 192.168.50.211 | 84:E3:42:50:1F:1E | pendiente-identificar |
| 192.168.50.213 | 3A:99:2B:ED:D3:5E | Caroline-tablet |
| 192.168.50.224 | D8:47:32:44:8A:79 | Tapo pendiente-identificar |
| 192.168.50.226 | 5A:BE:9F:4A:20:DD | iPad |
| 192.168.50.241 | CE:2A:16:A6:44:7D | Watch |
| 192.168.50.247 | 70:EE:50:86:FB:FE | pendiente-identificar |
| 192.168.50.248 | EC:5C:68:32:57:B0 | MiWiFi-R4A-srv0 |
## Siguiente prioridad
Control parental por dispositivo o grupo: horarios, bloqueo temporal, perfiles, pausas desde Telegram y registro de cambios. Se implementara despues de terminar la normalizacion de nombres del inventario.
## Despliegue ## Despliegue
La Pi no tiene Git operativo para este flujo. El despliegue se hace por el VPS y el tunel inverso: La Pi no tiene Git operativo para este flujo. El despliegue se hace por el VPS y el tunel inverso:

View File

@@ -392,6 +392,8 @@ def procesar(texto, chat_id):
"<code>/router estado</code> — modelo, firmware, IP y uptime\n" "<code>/router estado</code> — modelo, firmware, IP y uptime\n"
"<code>/router clientes</code> — tabla ARP de clientes\n" "<code>/router clientes</code> — tabla ARP de clientes\n"
"<code>/router wifi</code> — radios Wi-Fi\n" "<code>/router wifi</code> — radios Wi-Fi\n"
"<code>/router inventario</code> — leases DHCP con IP y MAC\n"
"<code>/router reservar confirmar</code> — fija las IP actuales\n"
"<code>/router reiniciar confirmar</code> — reinicio protegido", "<code>/router reiniciar confirmar</code> — reinicio protegido",
chat_id, chat_id,
) )
@@ -416,6 +418,24 @@ def procesar(texto, chat_id):
except Exception as e: except Exception as e:
log(f"router wifi error: {e}") log(f"router wifi error: {e}")
enviar(f"❌ Error consultando Wi-Fi: {e}", chat_id) enviar(f"❌ Error consultando Wi-Fi: {e}", chat_id)
elif accion_router == "inventario":
try:
from router_control import inventario
enviar("📋 <b>INVENTARIO DHCP ASUS</b>\n" + inventario(), chat_id)
except Exception as e:
log(f"router inventario error: {e}")
enviar(f"❌ Error consultando inventario: {e}", chat_id)
elif accion_router == "reservar":
if len(partes_router) < 2 or partes_router[1] != "confirmar":
enviar("Para reservar las IP actuales responde exactamente: <code>/router reservar confirmar</code>", chat_id)
else:
try:
from router_control import reservar_dhcp
enviar("⏳ Aplicando reservas DHCP...", chat_id)
enviar(f"✅ {reservar_dhcp()}", chat_id)
except Exception as e:
log(f"router reservar error: {e}")
enviar(f"❌ Error aplicando reservas: {e}", chat_id)
elif accion_router in ("reiniciar", "reboot"): elif accion_router in ("reiniciar", "reboot"):
if len(partes_router) < 2 or partes_router[1] != "confirmar": if len(partes_router) < 2 or partes_router[1] != "confirmar":
enviar("Para reiniciar responde exactamente: <code>/router reiniciar confirmar</code>", chat_id) enviar("Para reiniciar responde exactamente: <code>/router reiniciar confirmar</code>", chat_id)
@@ -488,7 +508,7 @@ def procesar(texto, chat_id):
" ✏️ Ej: <code>/estado</code>\n" " ✏️ Ej: <code>/estado</code>\n"
"<b>14.</b> <code>temperatura</code> — temperatura de la Pi\n" "<b>14.</b> <code>temperatura</code> — temperatura de la Pi\n"
" ✏️ Ej: <code>/temperatura</code>\n" " ✏️ Ej: <code>/temperatura</code>\n"
"<b>15.</b> <code>router estado|clientes|wifi</code> — controla el ASUS\n" "<b>15.</b> <code>router estado|clientes|wifi|inventario</code> — controla el ASUS\n"
" ✏️ Ej: <code>/router estado</code>\n" " ✏️ Ej: <code>/router estado</code>\n"
"<b>16.</b> <code>panorama</code> — fotos de las cámaras\n" "<b>16.</b> <code>panorama</code> — fotos de las cámaras\n"
" ✏️ Ej: <code>/panorama</code>\n" " ✏️ Ej: <code>/panorama</code>\n"

View File

@@ -1,6 +1,8 @@
"""Consultas seguras al ASUS RT-AX88U PRO desde la Raspberry.""" """Consultas seguras al ASUS RT-AX88U PRO desde la Raspberry."""
import html import html
import re
import shlex
import subprocess import subprocess
import sys import sys
@@ -59,11 +61,63 @@ def clientes():
return f"<pre>{html.escape(output)}</pre>" return f"<pre>{html.escape(output)}</pre>"
def _reservas_actuales(text):
return re.findall(r"<([^>]+)>([^>]+)>>", text or "")
def _inventario():
leases = _run("cat /var/lib/misc/dnsmasq.leases")
reservas = _reservas_actuales(_run("nvram get dhcp_staticlist"))
by_mac = {mac.upper(): ip for mac, ip in reservas}
devices = []
for line in leases.splitlines():
fields = line.split()
if len(fields) < 4 or not fields[2].startswith("192.168.50."):
continue
mac, ip, name = fields[1].upper(), fields[2], fields[3]
by_mac.setdefault(mac, ip)
devices.append((ip, mac, name if name != "*" else "pendiente-identificar"))
devices.sort(key=lambda item: int(item[0].rsplit(".", 1)[1]))
return devices, by_mac
def inventario():
devices, _by_mac = _inventario()
lines = ["IP MAC NOMBRE"]
lines.extend(f"{ip:<18} {mac:<22} {name}" for ip, mac, name in devices)
return f"<pre>{html.escape(chr(10).join(lines))}</pre>"
def reservar_dhcp():
devices, by_mac = _inventario()
payload = "".join(f"<{mac}>{ip}>>" for mac, ip in by_mac.items())
command = (
f"nvram set dhcp_staticlist={shlex.quote(payload)}; "
"nvram commit; service restart_dnsmasq"
)
result = _run(command)
if result.startswith("Error "):
return result
return f"Reservas DHCP aplicadas: {len(by_mac)} dispositivos. Leases inventariados: {len(devices)}."
def reservas():
output = _run("nvram get dhcp_staticlist")
return f"<pre>{html.escape(output)}</pre>"
def reiniciar(): def reiniciar():
return _run("reboot") return _run("reboot")
if __name__ == "__main__": if __name__ == "__main__":
action = sys.argv[1] if len(sys.argv) > 1 else "estado" action = sys.argv[1] if len(sys.argv) > 1 else "estado"
actions = {"estado": estado, "clientes": clientes, "wifi": wifi} actions = {
"estado": estado,
"clientes": clientes,
"wifi": wifi,
"inventario": inventario,
"reservar": reservar_dhcp,
"reservas": reservas,
}
print(actions.get(action, estado)()) print(actions.get(action, estado)())