mirror of
https://github.com/cheveguerra/TotalConnect.git
synced 2026-08-18 16:36:40 +00:00
Initial commit: Hybrid polling, interactive log modal and 3-second hard reset
This commit is contained in:
Executable
+15
@@ -0,0 +1,15 @@
|
||||
# Antigravity AI Rules
|
||||
|
||||
1. **Permiso explícito obligatorio para realizar modificaciones**:
|
||||
- El asistente de IA tiene permitido leer cualquier archivo y ejecutar comandos de lectura/diagnóstico sin necesidad de confirmación previa en el chat.
|
||||
- El asistente **NUNCA** debe crear, modificar o eliminar ningún archivo (tanto dentro como fuera del espacio de trabajo) ni ejecutar comandos que modifiquen el sistema sin la autorización previa y explícita del usuario en el chat.
|
||||
- Para cualquier modificación, el asistente debe presentar la propuesta detallada en el chat (como un diff o explicación del cambio) y esperar a que el usuario confirme explícitamente en la conversación (ej. "procede", "aplica", "ejecuta") antes de proceder.
|
||||
|
||||
2. **Persistencia de Planes y Tareas en el Proyecto**:
|
||||
- Para evitar pérdidas de contexto al cambiar o reiniciar sesiones de chat, el asistente siempre debe replicar y guardar copias de sus archivos de planificación, tareas y bitácoras (`implementation_plan.md`, `task.md`, `walkthrough.md`) en la carpeta `doc/` ubicada en la raíz del proyecto.
|
||||
- Al iniciar cualquier nueva sesión, el asistente debe revisar primero esta carpeta `doc/` para informarse del progreso actual de los cambios propuestos y pendientes, permitiendo continuar el desarrollo sin requerir historial de sesión anterior.
|
||||
|
||||
3. **Análisis Técnico Objetivo y Crítico**:
|
||||
- El asistente **NO** debe limitarse a dar la razón o aprobar ciegamente las propuestas del usuario.
|
||||
- Ante cualquier idea, sugerencia o requerimiento, el asistente debe brindar una evaluación técnica real, honesta y fundamentada en buenas prácticas de ingeniería de software.
|
||||
- **Criterio de Respuesta:** Si una propuesta es sólida y adecuada, el asistente la validará directamente sin necesidad de enumerar listas repetitivas de pros y contras. Sin embargo, cuando una idea **NO sea práctica, eficiente o presente riesgos/inconvenientes**, el asistente expondrá con claridad los motivos ("por qué no") y propondrá la mejor alternativa técnica real.
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
# Entornos virtuales y dependencias de Python/Node.js
|
||||
node_modules/
|
||||
venv/
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Archivos de configuración confidenciales (¡CRÍTICO!)
|
||||
credentials.json
|
||||
|
||||
# Cachés temporales e históricos de datos
|
||||
last_state.json
|
||||
locations_cache.json
|
||||
last_event_ids.json
|
||||
status.html
|
||||
|
||||
# Archivos de logs de depuración y ejecución
|
||||
*.log
|
||||
*.log.1
|
||||
server.log
|
||||
activity.log
|
||||
activity.log.1
|
||||
nohup.out
|
||||
|
||||
# Archivos temporales de sistemas operativos
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
@@ -0,0 +1,68 @@
|
||||
# TotalConnect 2.0 - Panel de Monitoreo de Alarmería
|
||||
|
||||
Este proyecto proporciona un panel web centralizado, responsivo y de alto rendimiento para el monitoreo consolidado de múltiples cuentas de alarmas de TotalConnect 2.0 (Resideo/Honeywell). Está diseñado para ejecutarse nativamente tanto en Linux como en Windows y generar reportes visuales dinámicos.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Requerimientos del Sistema y Servicios
|
||||
|
||||
### 1. Sistema Operativo y Servidor
|
||||
* **Linux (Debian/Ubuntu/Proxmox VM o similar):** Probado y validado en Debian/Ubuntu con Python 3.11.
|
||||
* **Windows (7/10/11 o Windows Server):** Ejecutable mediante el binario autónomo `totalconnect.exe` (compilado con PyInstaller).
|
||||
* **Acceso de Red:** El host debe tener salida a Internet (para conectarse al API de Total Connect) y ser accesible en la red local (para visualizar el panel en el puerto configurado).
|
||||
|
||||
### 2. Servicios de Sistema Recomendados
|
||||
* **En Linux:** Servicio Systemd (`totalconnect.service`) configurado para mantener el servidor web encendido de forma persistente y asegurar su auto-recuperación.
|
||||
* **En Windows:** Ejecutar el binario en segundo plano o utilizar el Programador de Tareas para actualizaciones silenciosas.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Dependencias de Python
|
||||
|
||||
Si ejecutas el script directamente desde el código fuente (`index.py`), se requiere:
|
||||
1. **`total-connect-client`:** Librería principal para interactuar con la API REST y OAuth2 de Total Connect.
|
||||
2. **`tzdata`:** Requerido en Windows para soportar zonas horarias locales (`America/Mexico_City`).
|
||||
|
||||
*Nota: Si utilizas el ejecutable compilado `totalconnect.exe`, estas dependencias ya vienen empaquetadas en su interior.*
|
||||
|
||||
---
|
||||
|
||||
## 📂 Estructura de Archivos del Proyecto
|
||||
|
||||
* **`totalconnect.exe` / `totalconnect`:** El binario ejecutable autónomo.
|
||||
* **`index.py`:** El script principal de código fuente en Python.
|
||||
* **`credentials.json`:** Archivo confidencial que contiene las credenciales y el puerto HTTP.
|
||||
* **`locations_cache.json`:** Historial temporal auto-generado de nombres de sucursales para soporte sin conexión.
|
||||
* **`status.html`:** Reporte final auto-generado.
|
||||
* **`activity.log`:** Archivo de logs de la aplicación.
|
||||
* **`activity.log.1`:** Respaldo anterior del log (se rota automáticamente al superar los 5 MB).
|
||||
* **`server.log`:** Registro de errores del sistema del comando de arranque en segundo plano.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Modos de Ejecución y Guía rápida
|
||||
|
||||
### Modo 1: Servidor Web Continuo (Por Defecto)
|
||||
Arranca el Servidor HTTP integrado. Lee el puerto del archivo `credentials.json` (por defecto `8080`).
|
||||
* **En Windows:** Haz doble clic en `totalconnect.exe` o corre en consola: `totalconnect.exe`
|
||||
* **En Linux:** Ejecuta `./totalconnect`
|
||||
* **Acceso:** Abre tu navegador en `http://localhost:PORT/` o `http://IP_DEL_SERVIDOR:PORT/`
|
||||
|
||||
### Modo 2: Actualización Única (On-Demand / Tareas Programadas)
|
||||
Consulta los estados una sola vez, genera `status.html` and finaliza. Ideal para automatizar con **Cron (Linux)** o **Programador de Tareas (Windows)**.
|
||||
* **Comando:** Agregar el argumento `--once` o `-o` (ej: `totalconnect.exe --once` o `./totalconnect -o`).
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Robustez y Resiliencia Implementada
|
||||
|
||||
* **Motor Híbrido Dinámico (27 + 7):** En cada ciclo de 30s se procesan 27 sucursales del turno ordinario (ordenadas secuencialmente por antigüedad) + hasta 7 de alta frecuencia (desarmadas/alarmadas/fallos), previniendo rate-limits.
|
||||
* **Decisión Inteligente (SOAP vs REST):** Realiza un Poll Lite SOAP (GetLiveEvents) rápido si los datos de la sucursal tienen menos de 10 minutos. Si detecta actividad física, programa un Poll Full REST (fullStatus) para la siguiente vuelta.
|
||||
* **Tope de Consultas REST:** Limitador integrado de máximo 35 consultas REST por ciclo para un arranque en frío seguro sin bloqueos de cuenta.
|
||||
* **Visor de Logs Interactiva (150 líneas):** Botón "Ver Log" en el dashboard para auditar el servidor en vivo directamente desde el navegador.
|
||||
* **Autogestión de Espacio:** Rotación automática de logs a los 5 MB de tamaño.
|
||||
* **Arranque Silencioso en Producción (VM):**
|
||||
`nohup ./venv/bin/python3 index.py > /dev/null 2> server.log &`
|
||||
Descarta prints de consola duplicados y deja `server.log` activo únicamente para atrapar excepciones de crash.
|
||||
* **Reinicio de Fábrica (Hard Reset) por Clic Largo (3s):**
|
||||
Al mantener presionado el botón "Actualizar" por 3 segundos, se purga la memoria y base de datos física para resolver inconsistencias. Cuenta con retraso de 1 segundo en el cambio de color para evitar destellos y cancelación silenciosa ergonómica si te arrepientes antes de completarse los 3 segundos.
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
import json
|
||||
from total_connect_client.client import TotalConnectClient
|
||||
from total_connect_client.const import make_http_endpoint
|
||||
|
||||
creds = json.load(open('credentials.json', encoding='utf-8'))['accounts'][0]
|
||||
client = TotalConnectClient(creds['username'], creds['password'], load_details=False)
|
||||
client.authenticate()
|
||||
|
||||
location_id = 1417027
|
||||
endpoint = make_http_endpoint(f"api/v3/locations/{location_id}/partitions/fullStatus")
|
||||
res = client.http_request(endpoint=endpoint, method="GET")
|
||||
|
||||
print("Keys at root of fullStatus response:")
|
||||
print(list(res.keys()))
|
||||
|
||||
print("\nValue of ArmingState at root:")
|
||||
print(res.get("ArmingState"))
|
||||
|
||||
print("\nValue of PanelStatus -> ArmingState:")
|
||||
panel_status = res.get("PanelStatus", {})
|
||||
print(panel_status.get("ArmingState"))
|
||||
|
||||
print("\nPartitions data:")
|
||||
for p in panel_status.get("Partitions", []):
|
||||
print(f" PartitionID={p.get('PartitionID')}, ArmingState={p.get('ArmingState')}")
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
# Análisis Comparativo: Arquitecturas Alternativas para el Monitoreo de TotalConnect
|
||||
|
||||
Este documento analiza dos enfoques técnicos alternativos para resolver el mismo problema de obtención de estatus de alarmas, comparándolos con nuestra implementación actual en **Python (REST API)**.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Cuadro Comparativo de Alternativas
|
||||
|
||||
| Característica | Enfoque 1: Python + REST API (Actual) | Enfoque 2: Node.js + Puppeteer (Scraping) | Enfoque 3: Go/Rust + REST/SOAP (Compilado) |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Consumo de Memoria** | Medio (~40-60 MB RAM) | Muy Alto (~150-350 MB RAM por navegador) | Mínimo (<15 MB RAM) |
|
||||
| **Tiempo de Respuesta** | Medio (~1-2 minutos por el ciclo secuencial) | Lento (~2-3 minutos por carga de interfaz) | Ultra Rápido (Segundos, por concurrencia nativa) |
|
||||
| **Robustez ante Cambios** | Alta (El API cambia muy rara vez) | Muy Baja (Cualquier cambio de HTML lo rompe) | Alta (El API cambia muy rara vez) |
|
||||
| **Complejidad del Código** | Baja/Media (Usa librerías existentes) | Media (Fácil de entender, difícil de depurar) | Alta (Requiere programar clientes HTTP y parsing a mano) |
|
||||
| **Facilidad de Despliegue**| Media (Requiere Python y venv) | Compleja (Requiere Node.js y dependencias de Chrome/X11) | Ultra Simple (Un único archivo binario compilado) |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Enfoque Alternativo A: Node.js + Puppeteer/Playwright (Web Scraping)
|
||||
|
||||
Consiste en programar un script en Node.js que levante un navegador "invisible" (Headless Chrome), entre a la página de inicio de sesión de Total Connect 2.0, ingrese el usuario y contraseña simulando clics humanos, navegue a la pantalla del teclado virtual y extraiga el texto y las clases CSS de las sucursales directamente del DOM del sitio web.
|
||||
|
||||
### 👍 Pros:
|
||||
* **Fidelidad al Usuario:** No depende de endpoints REST no documentados o secretos. Si funciona en la página web oficial, funcionará en el script.
|
||||
* **Manejo de Estados Visuales:** Obtiene exactamente lo que el usuario ve (los mismos íconos y textos que ya están renderizados en la web de Honeywell).
|
||||
|
||||
### 👎 Contras:
|
||||
* **Footprint de Recursos Masivo:** Ejecutar Chrome sin interfaz en un servidor Linux consume una cantidad enorme de CPU y memoria RAM. No es viable en servidores pequeños o con recursos limitados.
|
||||
* **Fragilidad del DOM:** Si Resideo actualiza el diseño de su página web (cambia un `id`, una clase de CSS o cambia la estructura de las etiquetas HTML), el scraper se rompe instantáneamente y requiere mantenimiento de emergencia.
|
||||
* **Dificultad con MFA:** Si la página de Honeywell decide exigir Verificación de Dos Pasos (MFA) o CAPTCHA en horarios de alta carga, simular el login a través de código es sumamente difícil de automatizar.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Enfoque Alternativo B: Go (Golang) o Rust + Clientes REST Nativos
|
||||
|
||||
Consiste en desarrollar un servicio binario en Go o Rust que se conecte directamente a los endpoints REST de Honeywell (haciendo las mismas llamadas OAuth2 que hace nuestro script en Python), compile un binario único optimizado y exponga un servidor HTTP ultraligero.
|
||||
|
||||
### 👍 Pros:
|
||||
* **Rendimiento Inigualable:** Un binario compilado en Go o Rust se ejecuta casi instantáneamente, consume menos de 15 MB de memoria RAM y no tiene dependencias en el servidor de producción (no necesita intérpretes de lenguaje, `venv` ni `node_modules`).
|
||||
* **Concurrencia Nativa (Velocidad de Carga):** Al tener soporte nativo de multiprocesamiento ligero (como las `goroutines` en Go), el script puede consultar las 100+ sucursales de forma **simultánea** en paralelo (en lugar de una por una en un ciclo secuencial). Esto reduciría el tiempo de actualización de 2 minutos a **menos de 5 segundos**.
|
||||
|
||||
### 👎 Contras:
|
||||
* **Curva de Aprendizaje y Desarrollo Lento:** No existen wrappers oficiales o librerías maduras de la comunidad para TotalConnect en Go o Rust. Habría que programar desde cero el flujo de autenticación OAuth2, la renovación del token de sesión y el mapeo de todas las complejas estructuras JSON y SOAP de Honeywell.
|
||||
* **Mantenibilidad:** Cualquier cambio o ajuste requiere volver a compilar el binario para la arquitectura del servidor destino.
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
# Explicación de Modos de Polling (Rápido vs. Full)
|
||||
|
||||
Este documento describe la arquitectura de consultas del monitor de TotalConnect 2.0, especificando el funcionamiento del **Polling Rápido (Ligero)** y del **Polling Completo (Profundo)**, así como las circunstancias en las que se ejecuta cada uno.
|
||||
|
||||
---
|
||||
|
||||
## 1. Polling Rápido (Ligero / Delta)
|
||||
|
||||
El Polling Rápido es un mecanismo altamente optimizado diseñado para detectar cambios de estado en tiempo real con el mínimo consumo de ancho de banda y rendimiento.
|
||||
|
||||
* **Tecnología Utilizada:**
|
||||
* **SOAP:** Método `GetLiveEvents` (usando el SessionID GUID).
|
||||
* **REST (Respaldo):** Endpoint `/sessiondetails` (ejecutado únicamente si SOAP está desactivado o falla).
|
||||
* **Funcionamiento:**
|
||||
* Solicita al servidor de Honeywell la lista de eventos ocurridos a partir de un ID de evento de referencia (`LastEventIdReceived`).
|
||||
* En modo de respaldo REST, solicita una lista compacta de las sucursales y sus estados básicos en una sola petición.
|
||||
* **No realiza consultas directas a los paneles físicos** de las sucursales, lo que permite que la respuesta tarde menos de 1 segundo para cientos de ubicaciones.
|
||||
* **Cuándo se ejecuta:**
|
||||
* **De forma automática y continua:** Cada **45 segundos** en el hilo de segundo plano del servidor (`background_poller_thread`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Polling Completo (Profundo / Full)
|
||||
|
||||
El Polling Completo obtiene el estado exhaustivo de una o todas las sucursales, interactuando directamente con el panel físico a través de la nube de Resideo.
|
||||
|
||||
* **Tecnología Utilizada:** Endpoint REST `/partitions/fullStatus` de cada sucursal.
|
||||
* **Funcionamiento:**
|
||||
* Consulta el estado en tiempo real de cada partición (P1-P6) y zona (Z1-Z285).
|
||||
* Extrae detalles críticos como fallos de zonas (puertas/ventanas abiertas), alarmas de fuego/gas/intrusión activas y horas localized de disparo de alarma.
|
||||
* Actualiza la persistencia local en `last_state.json` y regenera el reporte visual `status.html`.
|
||||
* **Cuándo se ejecuta:**
|
||||
|
||||
### A. Para sucursales individuales (Optimización Delta):
|
||||
Se ejecuta **únicamente para las sucursales específicas que cambiaron**, durante el ciclo automático de 45 segundos, si el Polling Rápido detecta alguna de las siguientes circunstancias:
|
||||
1. **Actividad reportada por SOAP:** Un evento nuevo en el delta de SOAP indica que hubo cambios en esa sucursal.
|
||||
2. **Cambio de estado general:** El estatus de armado básico en la respuesta REST de respaldo cambió respecto al valor en caché (ej: Desarmado ➔ Armado Stay).
|
||||
3. **Alarma activa:** La sucursal entra en un estado de alarma disparada (`ALARMING`, `ALARMING_FIRE_SMOKE`, etc.).
|
||||
4. **Validación de restablecimiento:** La sucursal tenía particiones disparadas en el ciclo anterior (necesario para verificar si ya se desactivó/silenció la alarma).
|
||||
5. **Nueva sucursal:** Se detecta una sucursal en la cuenta que no existía previamente en la caché.
|
||||
|
||||
### B. Para todas las sucursales de la cuenta (Fuerza Bruta):
|
||||
Se ejecuta una consulta completa para todas las sucursales de la cuenta bajo las siguientes circunstancias:
|
||||
1. **Refresco manual:** El usuario hace clic en el botón **"Actualizar"** en el panel web (lo que envía una solicitud POST a `/refresh` con `full_refresh=True`).
|
||||
2. **Arranque inicial sin caché:** El servidor inicia por primera vez y no existe el archivo `last_state.json`, lo que obliga a crear la base de datos de estatus desde cero.
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
# Guía de Empaquetado y Portabilidad: TotalConnect 2.0
|
||||
|
||||
Para hacer que este proyecto sea lo más portable posible y evitar configurar entornos virtuales (`venv`), instalar `pip` o gestionar dependencias manualmente en cada servidor, existen dos enfoques recomendados:
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Método 1: Empaquetar como un Ejecutable Único (PyInstaller)
|
||||
Este método compila tu script de Python y todas sus dependencias (incluyendo el intérprete de Python, las librerías `total-connect-client`, `zeep`, etc.) dentro de un **único archivo ejecutable binario para Linux**.
|
||||
|
||||
### 👍 Ventajas:
|
||||
* Genera un solo archivo (ej. `totalconnect`).
|
||||
* **Cero dependencias:** No necesitas tener instalado Python, `pip`, ni crear entornos virtuales en el servidor destino. Solo copias el archivo y lo corres.
|
||||
* Súper ligero (~15-20 MB).
|
||||
|
||||
### 🛠️ Cómo empaquetarlo (Ejecutar en la VM Linux `192.168.100.50`):
|
||||
|
||||
1. **Instalar PyInstaller en el entorno virtual actual:**
|
||||
```bash
|
||||
cd /mnt/data2/Software/Bot-WhatsApp/TotalConnect
|
||||
./venv/bin/pip install pyinstaller
|
||||
```
|
||||
|
||||
2. **Compilar el script en un único archivo:**
|
||||
```bash
|
||||
./venv/bin/pyinstaller --onefile --name totalconnect index.py
|
||||
```
|
||||
*(Este comando creará una carpeta `dist/` que contendrá el ejecutable final llamado `totalconnect`).*
|
||||
|
||||
3. **Cómo usar el ejecutable generado:**
|
||||
Puedes mover el archivo `dist/totalconnect` a cualquier parte (por ejemplo, a `/usr/local/bin/` para que sea un comando global) y correrlo directamente:
|
||||
* **Modo Servidor:** `./totalconnect --server 8080`
|
||||
* **Modo On-Demand:** `./totalconnect`
|
||||
|
||||
*Nota: Recuerda que `credentials.json` y `locations_cache.json` deben estar en la misma carpeta desde donde ejecutes el binario, ya que busca esos archivos en su directorio relativo.*
|
||||
|
||||
---
|
||||
|
||||
## 🐳 Método 2: Contenedorizar con Docker
|
||||
Este método empaqueta todo el entorno (sistema operativo base, Python, librerías y código) dentro de una imagen de Docker.
|
||||
|
||||
### 👍 Ventajas:
|
||||
* **Portabilidad absoluta:** Funciona de forma idéntica en cualquier sistema (Windows, macOS, Linux, AWS, Proxmox) que tenga instalado Docker.
|
||||
* **Aislamiento total:** No interfiere en lo absoluto con las librerías del sistema operativo.
|
||||
* Gestión de reinicios integrada (Docker se encarga de mantenerlo vivo).
|
||||
|
||||
### 🛠️ Archivos necesarios en la carpeta `/TotalConnect`:
|
||||
|
||||
1. **Crear un archivo `Dockerfile`:**
|
||||
```dockerfile
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Instalar dependencias
|
||||
RUN pip install --no-cache-dir total-connect-client
|
||||
|
||||
# Copiar código
|
||||
COPY index.py /app/index.py
|
||||
|
||||
# Exponer puerto del servidor
|
||||
EXPOSE 8080
|
||||
|
||||
# Comando para arrancar el servidor por defecto
|
||||
CMD ["python", "index.py", "--server", "8080"]
|
||||
```
|
||||
|
||||
2. **Construir la imagen de Docker:**
|
||||
```bash
|
||||
docker build -t totalconnect-monitor .
|
||||
```
|
||||
|
||||
3. **Ejecutar el contenedor (montando las credenciales para poder cambiarlas desde fuera):**
|
||||
```bash
|
||||
docker run -d \
|
||||
--name tc-monitor \
|
||||
--restart unless-stopped \
|
||||
-p 8080:8080 \
|
||||
-v $(pwd)/credentials.json:/app/credentials.json \
|
||||
-v $(pwd)/locations_cache.json:/app/locations_cache.json \
|
||||
totalconnect-monitor
|
||||
```
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
# Guía de Ubicación, Actualización y Despliegue del Proyecto
|
||||
|
||||
Esta guía documenta la estructura de directorios del proyecto, el flujo de sincronización y los comandos necesarios para iniciar, reiniciar y monitorear el servicio en la máquina virtual Linux por SSH.
|
||||
|
||||
---
|
||||
|
||||
## 1. Ubicaciones del Código
|
||||
|
||||
El proyecto reside en tres entornos clave:
|
||||
|
||||
1. **Copia de Desarrollo (Windows Local):**
|
||||
`E:\Software\TotalConnect\`
|
||||
*Uso:* Compilación del ejecutable standalone `totalconnect.exe` para Windows usando PyInstaller.
|
||||
2. **Copia Compartida (Samba Share):**
|
||||
`Z:\data2\Software\Bot-WhatsApp\TotalConnect\`
|
||||
*Uso:* Compartido en la red que la VM Linux monta de forma nativa. Cualquier cambio guardado en `Z:\` se refleja de forma instantánea en la VM.
|
||||
3. **Entorno de Producción (Linux VM - `192.168.100.50`):**
|
||||
`/mnt/data2/Software/Bot-WhatsApp/TotalConnect/`
|
||||
*Uso:* Directorio donde se ejecuta el demonio de polling de forma continua.
|
||||
|
||||
---
|
||||
|
||||
## 2. Flujo de Actualización de Cambios
|
||||
|
||||
Cada vez que se modifica el código (ej: en `index.py`), se debe seguir el siguiente orden para sincronizar los entornos:
|
||||
|
||||
1. Modificar el archivo principal en la red compartida:
|
||||
`Z:\data2\Software\Bot-WhatsApp\TotalConnect\index.py`
|
||||
2. Copiar los cambios a la ruta de desarrollo local en Windows:
|
||||
```powershell
|
||||
Copy-Item "Z:\data2\Software\Bot-WhatsApp\TotalConnect\index.py" "E:\Software\TotalConnect\index.py" -Force
|
||||
```
|
||||
3. Si el cambio requiere actualizar el ejecutable de Windows `totalconnect.exe`, ejecutar en Windows:
|
||||
```powershell
|
||||
cd E:\Software\TotalConnect\
|
||||
python -m PyInstaller --clean totalconnect.spec
|
||||
Copy-Item "E:\Software\TotalConnect\dist\totalconnect.exe" "E:\Software\TotalConnect\totalconnect.exe" -Force
|
||||
```
|
||||
4. Reiniciar el demonio en la VM (ver comandos en sección 3) para que lea las modificaciones de `index.py`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Ejecución del Servicio en la VM por SSH
|
||||
|
||||
El demonio de polling corre en la máquina virtual Linux en segundo plano. Para interactuar con él, conéctate por SSH:
|
||||
|
||||
```powershell
|
||||
ssh -i "C:\Users\JAGU\.gemini\antigravity-ide\brain\d979a063-dd63-47a8-8ec1-785abc40b475\scratch\id_ed25519_win" -o StrictHostKeyChecking=no root@192.168.100.50
|
||||
```
|
||||
|
||||
### Comandos de Control en la VM:
|
||||
|
||||
* **Detener el demonio activo:**
|
||||
```bash
|
||||
pkill -f 'python3 index.py'
|
||||
```
|
||||
* **Iniciar el demonio en segundo plano (Recomendado):**
|
||||
```bash
|
||||
cd /mnt/data2/Software/Bot-WhatsApp/TotalConnect
|
||||
nohup ./venv/bin/python3 index.py >> server.log 2>&1 &
|
||||
```
|
||||
* **Reiniciar el demonio (Detener e Iniciar en una línea):**
|
||||
```bash
|
||||
pkill -f 'python3 index.py'; sleep 2; cd /mnt/data2/Software/Bot-WhatsApp/TotalConnect && nohup ./venv/bin/python3 index.py >> server.log 2>&1 &
|
||||
```
|
||||
|
||||
### Comandos de Monitoreo:
|
||||
|
||||
* **Comprobar si el proceso de Python está activo:**
|
||||
```bash
|
||||
ps aux | grep 'python3 index.py' | grep -v grep
|
||||
```
|
||||
* **Visualizar los logs de actividad en tiempo real:**
|
||||
```bash
|
||||
tail -f /mnt/data2/Software/Bot-WhatsApp/TotalConnect/server.log
|
||||
```
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
# Plan de Implementación y Walkthrough Propuesto: Integración de Eventos en Tiempo Real
|
||||
|
||||
Este documento detalla el plan de diseño técnico y la arquitectura de los cambios que se implementarán en el proyecto TotalConnect (`index.py`) para integrar el monitoreo por eventos en tiempo real.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Objetivos de la Implementación
|
||||
|
||||
1. **Monitoreo Automático en Segundo Plano:** El servidor Python consultará a Honeywell mediante `GetEvents` cada 45–60 segundos sin requerir la intervención del usuario.
|
||||
2. **Actualización Ultra-Rápida (Delta Update):** En lugar de consultar las 285 sucursales (1-2 minutos), el sistema identificará únicamente las sucursales que registraron eventos nuevos y actualizará el reporte en **1 a 3 segundos**.
|
||||
3. **Cero Retraso al Abrir la Página (`GET /`):** La carga inicial seguirá leyendo `last_state.json` de inmediato (< 5 milisegundos).
|
||||
4. **Control Manual y Circuit Breaker (Botonera & Kill-Switch):**
|
||||
* Botón en la cabecera del dashboard para **Pausar / Reanudar Polling**.
|
||||
* Pausa automática inteligente si Honeywell responde errores de autenticación (401) o límite de peticiones (429).
|
||||
5. **DOM Patching Silencioso en la Interfaz (JS):** Actualización instantánea en el navegador únicamente de los nodos/tarjetas que cambiaron de estado, evitando parpadeos o animaciones molestas.
|
||||
6. **Manejo Resiliente Multicuenta y Revocados:** Las cuentas fuera de línea se mantendrán clasificadas con prioridad 3 al final del listado con fondo gris.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Cambios Arquitectónicos por Componente
|
||||
|
||||
### 1. Backend (`index.py`)
|
||||
|
||||
#### A. Hilo Demonio de Polling en Segundo Plano (`BackgroundPollerThread`)
|
||||
* Se agregará un `threading.Thread(daemon=True)` que se ejecuta automáticamente al arrancar el servidor.
|
||||
* Mantiene variables globales en memoria:
|
||||
* `polling_enabled` (`True` / `False`): Estado global del motor de consulta.
|
||||
* `last_event_ids`: Diccionario `{ "Ccontrol2020": 850123, "Ccontrol2019": 412099 }` con el último ID de evento procesado por cuenta.
|
||||
* `consecutive_errors`: Contador de fallos consecutivos por cuenta.
|
||||
|
||||
#### B. Flujo del Poller (`poll_events_cycle()`):
|
||||
1. **Verificación de Pausa:** Si `polling_enabled` es `False`, el hilo espera 15 segundos y re-evalúa.
|
||||
2. **Petición Delta `GetEvents`:**
|
||||
Para cada cuenta activa en `credentials.json`, envía una llamada ligera `GetEvents` pasando `LastEventIdReceived`.
|
||||
3. **Evaluación de Resultados:**
|
||||
* **Sin cambios:** Honeywell responde `[]`. No se hace nada más.
|
||||
* **Con cambios:** Extrae los `LocationID` afectados. Ejecuta `fullStatus` únicamente para esos IDs.
|
||||
4. **Actualización de Estado:** Consolida la información en `last_state.json` y vuelve a generar `status.html`.
|
||||
5. **Manejo de Errores (Circuit Breaker):** Si una cuenta recibe 2 errores `401` o `429` seguidos, el servidor pausa el polling automático para esa cuenta y notifica a la UI.
|
||||
|
||||
#### C. Endpoints HTTP Nuevos en `DashboardHandler`
|
||||
* `GET /polling-status`: Devuelve el estado actual (`enabled`, `last_run`, `active_errors`).
|
||||
* `POST /toggle-polling`: Permite a la interfaz web activar o pausar manualmente el polling.
|
||||
|
||||
---
|
||||
|
||||
### 2. Frontend (`status.html` / Template en `index.py`)
|
||||
|
||||
#### A. Botón de Control de Polling en Cabecera
|
||||
Junto al botón "Actualizar" y al selector de cuentas, se agregará un nuevo botón dinámico:
|
||||
```html
|
||||
<button id="toggle-polling-btn" class="btn-polling-active" onclick="togglePolling()">
|
||||
<span class="polling-indicator">●</span> Polling: Activo
|
||||
</button>
|
||||
```
|
||||
|
||||
#### B. Polling Local JS y DOM Patching
|
||||
Un script JS en el cliente ejecutará una consulta ultrarrápida a `GET /polling-status` cada 15 segundos:
|
||||
* Si la respuesta indica un cambio en el timestamp del último estado, realiza un `fetch('/')` silencioso.
|
||||
* Compara los estados de las sucursales y actualiza el HTML **únicamente de las tarjetas que cambiaron**, manteniendo la posición y el foco del usuario.
|
||||
* **Auto-Check por Visibilidad (`visibilitychange`):** Al volver a la pestaña tras estar inactivo, el navegador refresca los datos al instante.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Plan de Verificación
|
||||
|
||||
1. **Prueba de Polling en Segundo Plano:** Iniciar el servidor, alterar el estado de una sucursal de prueba y verificar en consola que la consulta toma < 2 segundos.
|
||||
2. **Prueba de Kill-Switch:** Presionar el botón "Pausar Polling" en la interfaz y verificar que cesan las llamadas HTTP a Honeywell.
|
||||
3. **Prueba de Circuit Breaker:** Simular una credencial inválida y verificar que el servidor desactiva automáticamente el polling tras 2 intentos fallidos.
|
||||
4. **Prueba de Rendimiento:** Confirmar que la carga inicial `GET /` sigue respondiendo en < 5ms.
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
# Walkthrough - Resumen de Mejoras y Correcciones (TotalConnect Client)
|
||||
|
||||
Este documento resume los cambios y optimizaciones realizados para robustecer el panel de monitoreo centralizado de alarmas.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Cambios Realizados
|
||||
|
||||
### 1. Actualización en Tiempo Real del Progreso de Carga
|
||||
* **Problema:** El modal de carga se quedaba en un mensaje estático ("Conectando a los paneles...") y no actualizaba qué sucursal estaba procesando.
|
||||
* **Solución:**
|
||||
* Se configuró la variable global `current_loading_status` para actualizarse en cada paso de `run_update()` (conexión inicial, selección de cuenta y consulta específica de sucursal).
|
||||
* Se agregaron cabeceras anti-caché en la respuesta HTTP (`Cache-Control: no-cache, no-store, must-revalidate`).
|
||||
* Se implementó un sondeo mediante `fetch` periódico (intervalo de 1.5s) que actualiza el texto de forma dinámica sin retrasos.
|
||||
|
||||
### 2. Lista de Exclusiones / Excepciones (`exclude`)
|
||||
* **Problema:** El usuario deseaba omitir ciertas sucursales de prueba o fuera de servicio de la visualización global.
|
||||
* **Solución:**
|
||||
* Se agregó soporte para una clave `"exclude": [...]` en el archivo de configuración `credentials.json`.
|
||||
* El script valida de forma inteligente si el ID numérico de la sucursal o su nombre coincide parcialmente con alguno de los elementos a excluir, omitiendo la consulta y el reporte de manera automática.
|
||||
|
||||
### 3. Apertura Automática del Navegador
|
||||
* **Problema:** Iniciar el ejecutable requería que el usuario abriera manualmente la página en `http://localhost:8080/`.
|
||||
* **Solución:**
|
||||
* Se integró el uso del módulo nativo `webbrowser` para abrir la pestaña al arrancar en modo servidor.
|
||||
* **Headless-safety:** El script detecta si el host no posee entorno gráfico (ausencia de `DISPLAY` en sistemas Linux/SSH) y desactiva esta apertura de forma segura para evitar cuelgues o warnings.
|
||||
* Se puede personalizar a través de `"open_browser": true/false` en el JSON o usando las banderas `--no-browser` / `-nb` en la CLI.
|
||||
|
||||
### 4. Filtrado de Historial de Alarma (Alarm Memory)
|
||||
* **Problema:** Algunas sucursales desarmadas o armadas normalmente mostraban alertas de alarma activas debido a memorias no limpiadas físicamente en el teclado Honeywell.
|
||||
* **Solución:**
|
||||
* Se rediseñó el bucle de validación de zonas. Ahora, una zona **sólo se marca como `ALARMA`** si la partición correspondiente de la sucursal está activamente disparada (`is_triggered`).
|
||||
* Si la sucursal está normal, las zonas cerradas no generan falsas alertas y las abiertas se muestran propiamente como `Abierta/Fallo` (color ámbar).
|
||||
|
||||
### 5. Tarjetas Deshabilitadas para Cuentas con Token Revocado
|
||||
* **Problema:** Cuando una cuenta de Total Connect pierde credenciales o se revoca, sus sucursales se cargaban de la caché local pero se visualizaban igual que las sucursales normales de cuentas activas.
|
||||
* **Solución:**
|
||||
* Se añadió la clase CSS `.branch-card-disabled` a las tarjetas cargadas desde la caché local con estatus `REVOKED`.
|
||||
* Esto les otorga un fondo gris medio (`#e2e8f0`), una opacidad del 65% y bloquea los efectos visuales de interacción en hover (sombra y escala), permitiendo distinguir a simple vista qué datos son históricos fuera de línea.
|
||||
|
||||
### 6. Diseño Responsivo y Text Wrapping
|
||||
* **Problema:** En zonas activas largas, las horas de disparo quedaban recortadas con puntos suspensivos en la tarjeta.
|
||||
* **Solución:**
|
||||
* Se eliminó el límite fijo de ancho de `.zone-desc` y se desactivó `white-space: nowrap`.
|
||||
* Se aumentó el ancho mínimo de las columnas en el grid responsivo a `290px`. Las descripciones largas ahora realizan un salto de línea natural posicionando la hora debajo de forma sumamente estética.
|
||||
|
||||
### 7. Checkboxes de Selección de Cuentas en la UI
|
||||
* **Problema:** El usuario deseaba poder elegir qué cuentas actualizar para evitar demoras innecesarias (por ejemplo, evitar actualizar cuentas revocadas).
|
||||
* **Solución:**
|
||||
* Se implementaron checkboxes en la cabecera que listan dinámicamente las cuentas de `credentials.json`.
|
||||
* Al hacer clic en "Actualizar", la UI envía mediante el cuerpo del POST (JSON) únicamente las etiquetas de las cuentas seleccionadas.
|
||||
|
||||
### 8. Persistencia y Mezcla de Estado (`last_state.json`)
|
||||
* **Problema:** Si se actualizaba únicamente una cuenta, el reporte en el frontend sobrescribía el archivo HTML y hacía desaparecer las demás sucursales.
|
||||
* **Solución:**
|
||||
* Se programó la escritura de `last_state.json` al finalizar cualquier consulta exitosa para guardar el último estado de todas las sucursales.
|
||||
* Cuando se realiza una actualización parcial (donde no se seleccionaron todas las cuentas), el backend carga los estados anteriores desde `last_state.json`, los mezcla con los nuevos resultados y genera la vista unificada.
|
||||
|
||||
### 9. Priorización de Cuentas Revocadas al Final del Listado
|
||||
* **Problema:** El usuario deseaba ver las cuentas inactivas/revocadas al final de la página para que no interfieran visualmente con las activas.
|
||||
* **Solución:**
|
||||
* Se definió un cuarto nivel de clasificación de prioridad:
|
||||
* `0`: Alarmas Activas (cabeza de página)
|
||||
* `1`: Sucursales Desarmadas
|
||||
* `2`: Sucursales Armadas (Stay, Away, etc.)
|
||||
* `3`: Sucursales Revocadas / Desconectadas (con fondo gris)
|
||||
* Todas las tarjetas dentro de cada categoría mantienen su estricto orden secuencial numérico.
|
||||
|
||||
### 10. Polling en Segundo Plano & Controles (Kill-Switch y Circuit Breaker)
|
||||
* **Problema:** El usuario requería monitoreo continuo sin bloqueos de API de Honeywell y con la capacidad de detener el polling manualmente o si Honeywell responde errores de autenticación/rate limit.
|
||||
* **Solución:**
|
||||
* **Motor Poller Daemon (`background_poller_thread`):** Se ejecuta cada 45s en segundo plano en el servidor Python.
|
||||
* **Circuit Breaker:** Si se registran 2 fallos de autenticación seguidos (`401` / `429`), el backend desactiva el polling automático previniendo bloqueos de IP de Honeywell.
|
||||
* **Endpoints Controladores:** Agregados `GET /polling-status` y `POST /toggle-polling`.
|
||||
* **Botonera en UI:** Se agregó el botón interactivo `[ ● Polling: Activo ]` / `[ ○ Polling: Pausado ]` en el encabezado.
|
||||
* **Auto-Focus (`visibilitychange`):** El navegador recarga el estatus automáticamente al regresar a la pestaña tras periodos de inactividad.
|
||||
|
||||
---
|
||||
|
||||
## 🔬 Verificación y Pruebas
|
||||
|
||||
* **Compilación y Empaquetado:** Generación exitosa de `totalconnect.exe` en Windows `E:\Software\TotalConnect\dist\totalconnect.exe`.
|
||||
* **Sincronización:** Actualizado y desplegado en segundo plano en la VM de Linux accesible a través del puerto `8080`.
|
||||
* **Validación de Funcionalidad:** Todas las rutas del dashboard, filtros por JSON, exclusiones y endpoints de polling operan correctamente.
|
||||
@@ -0,0 +1,517 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import axios from 'axios';
|
||||
import xml2js from 'xml2js';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const CREDENTIALS_FILE = path.join(__dirname, 'credentials.json');
|
||||
const HTML_OUTPUT_FILE = path.join(__dirname, 'status.html');
|
||||
const API_URL = 'https://rs.alarmnet.com/TC2API/TC2.asmx';
|
||||
const APPLICATION_ID = 14588; // App ID estándar de Total Connect 2.0
|
||||
const APPLICATION_VERSION = '3.0.0';
|
||||
|
||||
// Parser de XML configurado para remover prefijos de namespaces (como soap:)
|
||||
const xmlParser = new xml2js.Parser({
|
||||
explicitArray: false,
|
||||
ignoreAttrs: true,
|
||||
tagNameProcessors: [xml2js.processors.stripPrefix]
|
||||
});
|
||||
|
||||
// Función para parsear XML a objeto JS de manera segura
|
||||
async function parseXml(xmlString) {
|
||||
return await xmlParser.parseStringPromise(xmlString);
|
||||
}
|
||||
|
||||
// Inicializar archivo de credenciales de ejemplo si no existe
|
||||
function initCredentialsFile() {
|
||||
if (!fs.existsSync(CREDENTIALS_FILE)) {
|
||||
const template = [
|
||||
{
|
||||
label: "Cuenta Principal",
|
||||
username: "tu_usuario_1",
|
||||
password: "tu_password_1"
|
||||
},
|
||||
{
|
||||
label: "Cuenta Secundaria",
|
||||
username: "tu_usuario_2",
|
||||
password: "tu_password_2"
|
||||
}
|
||||
];
|
||||
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(template, null, 2), 'utf8');
|
||||
console.log(`⚠️ Se ha creado un archivo de plantilla en: ${CREDENTIALS_FILE}`);
|
||||
console.log("Por favor edítalo con tus credenciales reales de TotalConnect 2.0 antes de ejecutar.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Traducir código de estado de alarma
|
||||
function parseArmingState(stateCode) {
|
||||
const code = parseInt(stateCode);
|
||||
switch (code) {
|
||||
// Códigos normales de desarmado
|
||||
case 102:
|
||||
case 10200:
|
||||
case 10201: // Listo para armar
|
||||
case 10203: // Alarma en memoria / aviso
|
||||
return {
|
||||
label: "Desarmado",
|
||||
class: "state-disarmed",
|
||||
icon: "🟢",
|
||||
badge: "Listo"
|
||||
};
|
||||
case 10202: // No listo para armar (p.ej. sensor abierto)
|
||||
return {
|
||||
label: "Desarmado (No Listo / Zona Abierta)",
|
||||
class: "state-fault",
|
||||
icon: "🟡",
|
||||
badge: "Revisar"
|
||||
};
|
||||
// Códigos de armado
|
||||
case 101:
|
||||
case 10100: // Armado fuera (Away)
|
||||
return {
|
||||
label: "Armado Fuera",
|
||||
class: "state-armed-away",
|
||||
icon: "🔴",
|
||||
badge: "Armado (Away)"
|
||||
};
|
||||
case 103:
|
||||
case 10300: // Armado casa (Stay)
|
||||
return {
|
||||
label: "Armado En Casa",
|
||||
class: "state-armed-stay",
|
||||
icon: "🟠",
|
||||
badge: "Armado (Stay)"
|
||||
};
|
||||
case 104:
|
||||
case 10400: // Armado noche (Night)
|
||||
return {
|
||||
label: "Armado Noche",
|
||||
class: "state-armed-night",
|
||||
icon: "🔵",
|
||||
badge: "Armado (Night)"
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: `Estado Desconocido (${stateCode})`,
|
||||
class: "state-unknown",
|
||||
icon: "⚪",
|
||||
badge: "Desconocido"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Realizar Login y obtener SessionID y lista de Sucursales (Locations)
|
||||
async function loginAndGetLocations(username, password) {
|
||||
const soapEnvelope = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<soap:Body>
|
||||
<LoginAndGetSessionDetails xmlns="https://services.alarmnet.com/TC2/">
|
||||
<userName>${username}</userName>
|
||||
<password>${password}</password>
|
||||
<ApplicationID>${APPLICATION_ID}</ApplicationID>
|
||||
<ApplicationVersion>${APPLICATION_VERSION}</ApplicationVersion>
|
||||
<LocaleCode>en-US</LocaleCode>
|
||||
</LoginAndGetSessionDetails>
|
||||
</soap:Body>
|
||||
</soap:Envelope>`;
|
||||
|
||||
const response = await axios.post(API_URL, soapEnvelope, {
|
||||
headers: {
|
||||
'Content-Type': 'text/xml; charset=utf-8',
|
||||
'SOAPAction': 'https://services.alarmnet.com/TC2/LoginAndGetSessionDetails'
|
||||
}
|
||||
});
|
||||
|
||||
console.log("DEBUG RESPONSE:", response.data);
|
||||
const parsed = await parseXml(response.data);
|
||||
const result = parsed.Envelope.Body.LoginAndGetSessionDetailsResponse.LoginAndGetSessionDetailsResult;
|
||||
|
||||
if (result.ResultCode !== '0') {
|
||||
throw new Error(`Fallo en autenticación: Código de error ${result.ResultCode} (${result.ResultData || 'Credenciales inválidas'})`);
|
||||
}
|
||||
|
||||
const sessionId = result.SessionID;
|
||||
|
||||
// Normalizar la lista de locaciones (si es solo una, xml2js la deja como objeto en vez de array)
|
||||
let locations = [];
|
||||
const rawLocations = result.Locations?.LocationInfoBasic;
|
||||
if (rawLocations) {
|
||||
locations = Array.isArray(rawLocations) ? rawLocations : [rawLocations];
|
||||
}
|
||||
|
||||
return { sessionId, locations };
|
||||
}
|
||||
|
||||
// 2. Obtener el estatus de un panel específico (GetPanelMetaDataAndFullStatusEx_V2)
|
||||
async function getPanelStatus(sessionId, locationId) {
|
||||
const soapEnvelope = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<soap:Body>
|
||||
<GetPanelMetaDataAndFullStatusEx_V2 xmlns="https://services.alarmnet.com/TC2/">
|
||||
<SessionID>${sessionId}</SessionID>
|
||||
<LocationID>${locationId}</LocationID>
|
||||
<LastSequenceNumber>0</LastSequenceNumber>
|
||||
<LastUpdatedTimestampTicks>0</LastUpdatedTimestampTicks>
|
||||
<PartitionID>
|
||||
<int>1</int>
|
||||
</PartitionID>
|
||||
</GetPanelMetaDataAndFullStatusEx_V2>
|
||||
</soap:Body>
|
||||
</soap:Envelope>`;
|
||||
|
||||
const response = await axios.post(API_URL, soapEnvelope, {
|
||||
headers: {
|
||||
'Content-Type': 'text/xml; charset=utf-8',
|
||||
'SOAPAction': 'https://services.alarmnet.com/TC2/GetPanelMetaDataAndFullStatusEx_V2'
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = await parseXml(response.data);
|
||||
const result = parsed.Envelope.Body.GetPanelMetaDataAndFullStatusEx_V2Response.GetPanelMetaDataAndFullStatusEx_V2Result;
|
||||
|
||||
if (result.ResultCode !== '0') {
|
||||
throw new Error(`Fallo al consultar estatus del panel ${locationId}: Código ${result.ResultCode}`);
|
||||
}
|
||||
|
||||
return result.ArmingState;
|
||||
}
|
||||
|
||||
// 3. Generar el reporte HTML estático con diseño premium
|
||||
function generateHtmlReport(data) {
|
||||
const timeZone = 'America/Mexico_City';
|
||||
const timestamp = new Date().toLocaleString('es-MX', {
|
||||
timeZone,
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
|
||||
// Construir filas de la tabla de sucursales
|
||||
let rowsHtml = '';
|
||||
data.forEach(account => {
|
||||
account.locations.forEach(loc => {
|
||||
const state = parseArmingState(loc.status);
|
||||
rowsHtml += `
|
||||
<tr>
|
||||
<td>
|
||||
<div class="account-badge">${account.label}</div>
|
||||
<div class="username-sub">${account.username}</div>
|
||||
</td>
|
||||
<td class="location-name">
|
||||
🏢 ${loc.name}
|
||||
<div class="location-id">ID: ${loc.id}</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge ${state.class}">
|
||||
<span class="status-dot"></span>
|
||||
${state.label}
|
||||
</span>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
});
|
||||
|
||||
const htmlContent = `<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Estatus de Sucursales - TotalConnect</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-gradient: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
|
||||
--panel-bg: rgba(30, 41, 59, 0.7);
|
||||
--border-color: rgba(255, 255, 255, 0.08);
|
||||
--text-main: #f8fafc;
|
||||
--text-muted: #94a3b8;
|
||||
|
||||
/* Colores de estados */
|
||||
--disarmed-color: #10b981;
|
||||
--disarmed-bg: rgba(16, 185, 129, 0.15);
|
||||
--fault-color: #f59e0b;
|
||||
--fault-bg: rgba(245, 158, 11, 0.15);
|
||||
--armed-away-color: #ef4444;
|
||||
--armed-away-bg: rgba(239, 68, 68, 0.15);
|
||||
--armed-stay-color: #f97316;
|
||||
--armed-stay-bg: rgba(249, 115, 22, 0.15);
|
||||
--armed-night-color: #3b82f6;
|
||||
--armed-night-bg: rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Outfit', sans-serif;
|
||||
background: var(--bg-gradient);
|
||||
color: var(--text-main);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 90%;
|
||||
max-width: 900px;
|
||||
margin: 40px auto;
|
||||
background: var(--panel-bg);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 24px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 24px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 2.2rem;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(to right, #60a5fa, #c084fc);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.timestamp-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.timestamp-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #c084fc;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.account-badge {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
color: #a5b4fc;
|
||||
padding: 4px 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.username-sub {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.location-name {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.location-id {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Estilo de Badges de Estado */
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 8px currentColor;
|
||||
}
|
||||
|
||||
.state-disarmed {
|
||||
background: var(--disarmed-bg);
|
||||
color: var(--disarmed-color);
|
||||
}
|
||||
.state-fault {
|
||||
background: var(--fault-bg);
|
||||
color: var(--fault-color);
|
||||
}
|
||||
.state-armed-away {
|
||||
background: var(--armed-away-bg);
|
||||
color: var(--armed-away-color);
|
||||
}
|
||||
.state-armed-stay {
|
||||
background: var(--armed-stay-bg);
|
||||
color: var(--armed-stay-color);
|
||||
}
|
||||
.state-armed-night {
|
||||
background: var(--armed-night-bg);
|
||||
color: var(--armed-night-color);
|
||||
}
|
||||
.state-unknown {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 40px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
/* Animación sutil */
|
||||
.status-dot {
|
||||
animation: pulse 2s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 0.6; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<div>
|
||||
<h1>TotalConnect 2.0</h1>
|
||||
<div style="font-size: 0.95rem; color: var(--text-muted); margin-top: 4px;">Monitoreo centralizado de alarmas</div>
|
||||
</div>
|
||||
<div class="timestamp">
|
||||
<div class="timestamp-label">Última Actualización</div>
|
||||
<div class="timestamp-value">${timestamp}</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cuenta</th>
|
||||
<th>Sucursal / Locación</th>
|
||||
<th>Estatus Alarma</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rowsHtml}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<footer>
|
||||
Generado automáticamente on-demand • TotalConnect API Client
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
fs.writeFileSync(HTML_OUTPUT_FILE, htmlContent, 'utf8');
|
||||
console.log(`\n🎉 Reporte HTML premium generado con éxito en: ${HTML_OUTPUT_FILE}`);
|
||||
}
|
||||
|
||||
// Función principal
|
||||
async function main() {
|
||||
console.log("🚀 Iniciando cliente de consulta TotalConnect 2.0...");
|
||||
initCredentialsFile();
|
||||
|
||||
const rawCreds = fs.readFileSync(CREDENTIALS_FILE, 'utf8');
|
||||
let accounts = JSON.parse(rawCreds);
|
||||
if (!Array.isArray(accounts) && accounts.accounts) {
|
||||
accounts = accounts.accounts;
|
||||
}
|
||||
|
||||
const reportData = [];
|
||||
|
||||
for (const acc of accounts) {
|
||||
console.log(`\n👤 Conectando a cuenta: ${acc.label} (${acc.username})...`);
|
||||
try {
|
||||
const { sessionId, locations } = await loginAndGetLocations(acc.username, acc.password);
|
||||
console.log(` ✓ Sesión establecida con éxito.`);
|
||||
console.log(` ✓ Se encontraron ${locations.length} sucursal(es).`);
|
||||
|
||||
const locDataList = [];
|
||||
for (const loc of locations) {
|
||||
console.log(` ⏳ Consultando estatus de sucursal: ${loc.LocationName}...`);
|
||||
const armingState = await getPanelStatus(sessionId, loc.LocationID);
|
||||
locDataList.push({
|
||||
id: loc.LocationID,
|
||||
name: loc.LocationName,
|
||||
status: armingState
|
||||
});
|
||||
}
|
||||
reportData.push({
|
||||
label: acc.label,
|
||||
username: acc.username,
|
||||
locations: locDataList
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(` ❌ Error procesando cuenta ${acc.label}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (reportData.length > 0) {
|
||||
generateHtmlReport(reportData);
|
||||
} else {
|
||||
console.error("❌ No se pudieron recuperar datos de ninguna cuenta.");
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error("❌ Error catastrófico en la ejecución principal:", err);
|
||||
});
|
||||
Generated
+379
@@ -0,0 +1,379 @@
|
||||
{
|
||||
"name": "totalconnect",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "totalconnect",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.18.1",
|
||||
"xml2js": "^0.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
|
||||
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.4",
|
||||
"mime-types": "^2.1.35"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
|
||||
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xml2js": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
||||
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "totalconnect",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.18.1",
|
||||
"xml2js": "^0.6.2"
|
||||
}
|
||||
}
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
state_file = 'last_state.json'
|
||||
if os.path.exists(state_file):
|
||||
with open(state_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
print("Content of last_state.json:")
|
||||
for acc in data:
|
||||
print(f"\nAccount: {acc.get('label')} ({acc.get('username')})")
|
||||
for loc in acc.get('locations', []):
|
||||
loc_id = loc.get('id')
|
||||
loc_name = loc.get('name')
|
||||
if '0181' in loc_name or loc_id == 1417027:
|
||||
print(f" 🏢 MATCH: ID={loc_id}, Name='{loc_name}', Status={loc.get('status')}")
|
||||
print(f" Triggered Partitions: {loc.get('triggered_partitions')}")
|
||||
print(f" Active Zones: {loc.get('active_zones')}")
|
||||
else:
|
||||
print("last_state.json does not exist!")
|
||||
Reference in New Issue
Block a user