commit 7808501bf4a888a6053d176183b87229d2d4503e Author: cheveguerra Date: Fri Jul 24 15:31:56 2026 +0000 Initial commit: Hybrid polling, interactive log modal and 3-second hard reset diff --git a/.antigravityrules b/.antigravityrules new file mode 100755 index 0000000..1f00ffd --- /dev/null +++ b/.antigravityrules @@ -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. \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..0e104f7 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100755 index 0000000..503d6b9 --- /dev/null +++ b/README.md @@ -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. diff --git a/check_fullstatus_json.py b/check_fullstatus_json.py new file mode 100755 index 0000000..e4701ab --- /dev/null +++ b/check_fullstatus_json.py @@ -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')}") diff --git a/doc/alternative_implementations_comparison.md b/doc/alternative_implementations_comparison.md new file mode 100755 index 0000000..b67282a --- /dev/null +++ b/doc/alternative_implementations_comparison.md @@ -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. diff --git a/doc/polling_modes_explanation.md b/doc/polling_modes_explanation.md new file mode 100755 index 0000000..00fc08d --- /dev/null +++ b/doc/polling_modes_explanation.md @@ -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. diff --git a/doc/portability_and_packaging.md b/doc/portability_and_packaging.md new file mode 100755 index 0000000..89eb730 --- /dev/null +++ b/doc/portability_and_packaging.md @@ -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 + ``` diff --git a/doc/project_deployment_guide.md b/doc/project_deployment_guide.md new file mode 100755 index 0000000..2ceeae0 --- /dev/null +++ b/doc/project_deployment_guide.md @@ -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 + ``` diff --git a/doc/proposed_events_integration_plan.md b/doc/proposed_events_integration_plan.md new file mode 100755 index 0000000..30fbeab --- /dev/null +++ b/doc/proposed_events_integration_plan.md @@ -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 + +``` + +#### 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. diff --git a/doc/walkthrough.md b/doc/walkthrough.md new file mode 100755 index 0000000..a0d58d6 --- /dev/null +++ b/doc/walkthrough.md @@ -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. diff --git a/index.js b/index.js new file mode 100755 index 0000000..da0c3c6 --- /dev/null +++ b/index.js @@ -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 = ` + + + + ${username} + ${password} + ${APPLICATION_ID} + ${APPLICATION_VERSION} + en-US + + +`; + + 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 = ` + + + + ${sessionId} + ${locationId} + 0 + 0 + + 1 + + + +`; + + 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 += ` + + +
${account.label}
+
${account.username}
+ + + 🏢 ${loc.name} +
ID: ${loc.id}
+ + + + + ${state.label} + + + `; + }); + }); + + const htmlContent = ` + + + + + Estatus de Sucursales - TotalConnect + + + + + + +
+
+
+

TotalConnect 2.0

+
Monitoreo centralizado de alarmas
+
+
+
Última Actualización
+
${timestamp}
+
+
+ + + + + + + + + + + ${rowsHtml} + +
CuentaSucursal / LocaciónEstatus Alarma
+ +
+ Generado automáticamente on-demand • TotalConnect API Client +
+
+ +`; + + 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); +}); diff --git a/index.py b/index.py new file mode 100755 index 0000000..ecad318 --- /dev/null +++ b/index.py @@ -0,0 +1,2462 @@ +# -*- coding: utf-8 -*- +""" +================================================================================ +INSTRUCCIONES DE COMPILACIÓN Y USO (PORTABILIDAD Y ARQUITECTURA) +================================================================================ + +Este script puede ser empaquetado en un único archivo ejecutable binario (.exe en Windows +o binario nativo en Linux) que se puede ejecutar en cualquier máquina de su misma +arquitectura sin necesidad de instalar Python o dependencias adicionales. + +-------------------------------------------------------------------------------- +1. REQUISITOS PREVIOS (Instalación de Python y PyInstaller) +-------------------------------------------------------------------------------- +Si la máquina de desarrollo NO tiene instalado Python o PyInstaller: + +A) Instalar Python: + - Windows: Descargue el instalador desde https://www.python.org/ + Asegúrese de marcar la casilla "Add Python.exe to PATH" durante la instalación. + - Linux (Debian/Ubuntu): Viene preinstalado. Si no, instálelo usando: + sudo apt update && sudo apt install python3 python3-pip python3-venv + +B) Instalar Dependencias del Script: + Abra la terminal (cmd/PowerShell en Windows, o bash en Linux) y ejecute: + pip install total-connect-client pyinstaller tzdata + +-------------------------------------------------------------------------------- +2. COMANDOS DE COMPILACIÓN +-------------------------------------------------------------------------------- +Ejecute el comando correspondiente según el sistema operativo en la carpeta del script: + +A) En Windows: + - Para generar un ejecutable estándar con ventana negra de consola: + python -m PyInstaller --onefile --name totalconnect index.py + + - Para generar un ejecutable silencioso en segundo plano (sin ventana negra): + python -m PyInstaller --onefile --noconsole --name totalconnect index.py + +B) En Linux: + - Para generar el ejecutable binario nativo de Linux: + python3 -m PyInstaller --onefile --name totalconnect index.py + +-------------------------------------------------------------------------------- +3. INSTRUCCIONES DE USO DEL EJECUTABLE +-------------------------------------------------------------------------------- +Una vez compilado, busque el ejecutable generado dentro de la carpeta "dist/". + +- Mueva o copie el ejecutable a la carpeta que desee. +- Copie el archivo 'credentials.json' a esa misma carpeta (al lado del ejecutable). +- Estatus del Servidor Web (Inicia Servidor Web en http://localhost:PORT/): + - Windows: Doble clic en 'totalconnect.exe' (se iniciará como servidor y abrirá el navegador de inmediato). + - Linux: ./totalconnect + - El arranque es INSTANTÁNEO. Si no hay reporte previo ('status.html'), genera un placeholder limpio indicando que debe hacer clic en "Actualizar" para su primera carga. + - Parámetros CLI de Navegador: Puede usar '--no-browser' o '-nb' para evitar que se abra la ventana del navegador. +- Ejecución Única (On-Demand / Guarda HTML y sale): + - Ejecutar agregando el argumento '--once' o '-o' (ej: totalconnect.exe --once o ./totalconnect -o) + - Sirve para programar el script mediante el Programador de Tareas en Windows o el demonio Cron en Linux. + +-------------------------------------------------------------------------------- +4. FORMATOS SOPORTADOS PARA 'credentials.json' +-------------------------------------------------------------------------------- +El archivo 'credentials.json' debe colocarse junto al ejecutable y soporta dos formatos: + +Formato A (Formato por objeto con puerto, exclusiones y navegador - RECOMENDADO): +{ + "port": 8080, + "exclude": ["1071", "1171", "1215"], // Lista de sucursales a ignorar (ID, número o nombre) + "open_browser": true, // true para abrir navegador al iniciar (defecto en GUI) + "accounts": [ + { + "label": "2020", + "username": "TuUsuario2020", + "password": "TuPassword2020" + }, + { + "label": "2019", + "username": "TuUsuario2019", + "password": "TuPassword2019" + } + ] +} + +Formato B (Formato original de lista - Puerto por defecto 8080): +[ + { + "label": "2020", + "username": "TuUsuario2020", + "password": "TuPassword2020" + }, + { + "label": "2019", + "username": "TuUsuario2019", + "password": "TuPassword2019" + } +] + +-------------------------------------------------------------------------------- +5. MOTOR DE POLLING HÍBRIDO Y AUDITORÍA EN VIVO (Mantenimiento Inteligente) +-------------------------------------------------------------------------------- +Este script implementa un motor de polling de alto rendimiento optimizado contra rate-limits: + +A) Motor Dinámico Híbrido (Lote 27 + 7 por ciclo de 30s): + - Cada 30 segundos, consulta a 27 sucursales del turno ordinario (ordenadas de forma + persistente por antigüedad de verificación) + hasta 7 de alta frecuencia (paneles + desarmados, en alarma o con fallas físicas). + - Realiza un Poll Lite SOAP (GetLiveEvents) rápido si la antigüedad de datos es menor + a 10 minutos. Si detecta actividad física, programa un Poll Full REST (fullStatus) + para el siguiente ciclo de 30s. + - Realiza un Poll Full REST de mantenimiento automático si la antigüedad supera los + 10 minutos, garantizando coherencia de datos con Honeywell. + - Limitador integrado de velocidad de máximo 35 Polls Full por ciclo para prevenir + bloqueos de cuenta en arranques en frío. + +B) Visor de Logs y Autogestión de Almacenamiento: + - Se incluye un botón interactivo "Ver Log" en la cabecera del dashboard para abrir + una terminal oscura en el navegador con las últimas 150 líneas del log de actividad. + - Rotación automática de 'activity.log' en caliente al alcanzar los 5 MB de tamaño, + preservando un respaldo anterior ('activity.log.1'). + - En producción, es recomendable iniciar el daemon silenciosamente en Linux: + nohup ./venv/bin/python3 index.py > /dev/null 2> server.log & + Esto descarta la consola redundante (que ya va a activity.log) y mantiene + 'server.log' ocupado únicamente con tracebacks de error reales del sistema. + +C) Reinicio de Fábrica (Hard Reset) por Clic Largo (3 segundos): + - Al mantener presionado el botón "Actualizar" por 3 segundos completos, se ejecuta + un Hard Reset (limpieza de cachés de disco, purga de memoria de sesiones de clientes + y reinicio de escaneo de cero). + - Ergonomía del botón: + * Clic Rápido (<300ms): Ejecuta un refresco clásico en caliente de la UI. + * Clic Medio / Cancelado (300ms a 3000ms): Cancela de forma silenciosa ("aquí no pasó nada"). + * Retraso Visual de 1s: El botón no se pinta en rojo ni cambia su texto hasta cruzar + el primer segundo (1000ms) de presión continua, evitando destellos molestos en + clics habituales. +================================================================================ +""" +import os +import sys +import json +import time +import datetime +import threading +import http.server +import socketserver +import webbrowser +import platform +import logging +from socketserver import ThreadingMixIn +from zoneinfo import ZoneInfo +import requests +import xml.etree.ElementTree as ET +from total_connect_client.client import TotalConnectClient +from total_connect_client.const import ArmingState, make_http_endpoint + +# Silenciar advertencias internas irrelevantes de la librería TotalConnect Client +logging.getLogger("total_connect_client").setLevel(logging.ERROR) + +# Configurar codificación UTF-8 para la consola (evita UnicodeEncodeError con emojis en Windows) +if sys.stdout is not None: + try: + sys.stdout.reconfigure(encoding='utf-8') + except Exception: + pass +if sys.stderr is not None: + try: + sys.stderr.reconfigure(encoding='utf-8') + except Exception: + pass + +# Directorio del script (soporta empaquetado con PyInstaller) +if getattr(sys, 'frozen', False): + BASE_DIR = os.path.dirname(sys.executable) +else: + BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +CREDENTIALS_FILE = os.path.join(BASE_DIR, 'credentials.json') +HTML_OUTPUT_FILE = os.path.join(BASE_DIR, 'status.html') +CACHE_FILE = os.path.join(BASE_DIR, 'locations_cache.json') +LAST_STATE_FILE = os.path.join(BASE_DIR, 'last_state.json') +LAST_EVENT_IDS_FILE = os.path.join(BASE_DIR, 'last_event_ids.json') +LOG_FILE = os.path.join(BASE_DIR, 'activity.log') +polling_trigger_event = threading.Event() +soap_disabled_accounts = set() +soap_session_ids = {} # dict: username -> GUID SessionId para SOAP GetLiveEvents + +def load_last_event_ids(): + """Carga los últimos IDs de eventos SOAP guardados en disco.""" + if os.path.exists(LAST_EVENT_IDS_FILE): + try: + with open(LAST_EVENT_IDS_FILE, 'r', encoding='utf-8') as f: + data = json.load(f) + if isinstance(data, dict): + return data + except Exception as e: + log_msg(f"⚠️ No se pudieron cargar los IDs de eventos: {e}") + return {} + +def save_last_event_ids(event_ids): + """Guarda los últimos IDs de eventos SOAP en disco.""" + try: + with open(LAST_EVENT_IDS_FILE, 'w', encoding='utf-8') as f: + json.dump(event_ids, f, ensure_ascii=False, indent=2) + except Exception as e: + log_msg(f"⚠️ No se pudieron guardar los IDs de eventos: {e}") + +def load_polling_state(): + """Carga el estado del polling (activo/pausado) exclusivamente desde credentials.json.""" + if os.path.exists(CREDENTIALS_FILE): + try: + with open(CREDENTIALS_FILE, 'r', encoding='utf-8') as f: + data = json.load(f) + if isinstance(data, dict): + return data.get("polling_enabled", True) + except Exception: + pass + return True + +def save_polling_state(enabled): + """Guarda el estado del polling (activo/pausado) exclusivamente en credentials.json.""" + target_files = [CREDENTIALS_FILE] + if platform.system() == "Windows": + target_files += [ + r"Z:\data2\Software\Bot-WhatsApp\TotalConnect\credentials.json", + r"E:\Software\TotalConnect\credentials.json", + r"E:\Software\TotalConnect\dist\credentials.json" + ] + + for target_path in set(target_files): + if os.path.exists(target_path): + try: + with open(target_path, 'r', encoding='utf-8') as f: + creds_data = json.load(f) + if isinstance(creds_data, dict): + creds_data["polling_enabled"] = enabled + with open(target_path, 'w', encoding='utf-8') as f: + json.dump(creds_data, f, ensure_ascii=False, indent=2) + log_msg(f"💾 Estado de polling ({enabled}) guardado en: {target_path}") + except Exception as e: + log_msg(f"⚠️ No se pudo actualizar polling_enabled en {target_path}: {e}") + +polling_enabled = load_polling_state() + +def log_msg(msg): + """Escribe un mensaje de log con timestamp tanto en la consola (con flush) como en activity.log.""" + try: + now_str = datetime.datetime.now(ZoneInfo("America/Mexico_City")).strftime('%Y-%m-%d %I:%M:%S %p') + formatted = f"[{now_str}] {msg}" + except Exception: + formatted = f"[LOG] {msg}" + print(formatted, flush=True) + + # Rotar activity.log si supera los 5 MB + try: + if os.path.exists(LOG_FILE) and os.path.getsize(LOG_FILE) > 5 * 1024 * 1024: + backup_file = LOG_FILE + ".1" + if os.path.exists(backup_file): + os.remove(backup_file) + os.rename(LOG_FILE, backup_file) + except Exception: + pass + + try: + with open(LOG_FILE, 'a', encoding='utf-8') as f: + f.write(formatted + '\n') + except Exception: + pass + +update_lock = threading.Lock() +current_loading_status = "" +polling_lock = threading.Lock() +last_event_ids = load_last_event_ids() +consecutive_errors = {} +last_poll_timestamp = "Nunca" +client_pool = {} +client_pool_lock = threading.Lock() + + + +README_CONTENT = """# 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` y 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. +""" + +def auto_generate_docs(): + """Genera automáticamente el archivo README.md y credentials.json si no existen.""" + # Generar README.md si no existe + readme_path = os.path.join(BASE_DIR, 'README.md') + if not os.path.exists(readme_path): + try: + with open(readme_path, 'w', encoding='utf-8') as f: + f.write(README_CONTENT) + print("📝 Se ha creado el archivo de documentación README.md de forma automática.") + except Exception as e: + print(f"⚠️ No se pudo auto-generar README.md: {e}") + + # Generar plantilla de credentials.json si no existe + if not os.path.exists(CREDENTIALS_FILE): + dummy_credentials = { + "port": 8080, + "accounts": [ + { + "label": "CUENTA_EJEMPLO", + "username": "TuUsuarioTotalConnect", + "password": "TuPasswordTotalConnect" + } + ] + } + try: + with open(CREDENTIALS_FILE, 'w', encoding='utf-8') as f: + json.dump(dummy_credentials, f, ensure_ascii=False, indent=2) + print(f"🔑 Se ha creado una plantilla de credenciales en: {CREDENTIALS_FILE}") + print(" Por favor edita este archivo con tus credenciales reales de TotalConnect.") + except Exception as e: + print(f"⚠️ No se pudo crear la plantilla de credenciales: {e}") + +def parse_arming_state(state): + """Mapea los estados de ArmingState de total-connect-client a etiquetas legibles.""" + if isinstance(state, str) and state == "REVOKED": + return { + "label": "Token Revocado / Error", + "class": "state-revoked", + "badge": "Desconectado" + } + + if isinstance(state, int): + try: + state = ArmingState(state) + except Exception: + state = ArmingState.UNKNOWN + + if hasattr(state, 'is_disarmed') and state.is_disarmed(): + if state == ArmingState.DISARMED_ZONE_FAULTED: + return { + "label": "Desarmado (Zona Abierta / Fallo)", + "class": "state-fault", + "badge": "Revisar" + } + return { + "label": "Desarmado", + "class": "state-disarmed", + "badge": "Listo" + } + elif hasattr(state, 'is_armed_away') and state.is_armed_away(): + return { + "label": "Armado Away", + "class": "state-armed-away", + "badge": "Armado (Away)" + } + elif hasattr(state, 'is_armed_home') and (state.is_armed_home() or state.is_armed_night() or state.is_armed_custom_bypass()): + return { + "label": "Armado Stay", + "class": "state-armed-stay", + "badge": "Armado (Stay)" + } + elif hasattr(state, 'is_triggered') and state.is_triggered(): + return { + "label": "¡ALARMA ACTIVA!", + "class": "state-alarm", + "badge": "¡ALERTA!" + } + elif hasattr(state, 'is_pending') and state.is_pending(): + return { + "label": "Procesando Cambio...", + "class": "state-pending", + "badge": "Pendiente" + } + else: + state_name = getattr(state, 'name', str(state)) + return { + "label": f"Desconocido ({state_name})", + "class": "state-unknown", + "badge": "Desconocido" + } + +def format_trigger_time(raw_time_str): + """Formatea la hora de disparo de alarma. Si contiene 'Z', convierte de UTC a CDMX. Si no, formatea la hora local existente.""" + if not raw_time_str: + return "" + try: + s = str(raw_time_str).strip() + if 'T' in s: + if s.endswith('Z'): + clean_str = s.rstrip('Z') + dt = datetime.datetime.fromisoformat(clean_str).replace(tzinfo=datetime.timezone.utc) + dt_cdmx = dt.astimezone(ZoneInfo("America/Mexico_City")) + return f" @ {dt_cdmx.strftime('%I:%M:%S %p')}" + else: + dt = datetime.datetime.fromisoformat(s) + return f" @ {dt.strftime('%I:%M:%S %p')}" + return f" @ {s}" + except Exception: + try: + return f" @ {str(raw_time_str).split('T')[1].rstrip('Z')}" + except Exception: + return f" @ {raw_time_str}" + + +def _get_location_sort_priority(loc_item): + """Calcula la prioridad de ordenamiento de una sucursal según su estado de armado.""" + state = loc_item['status'] + if isinstance(state, str) and state == "REVOKED": + return 3 + if isinstance(state, int): + try: + state = ArmingState(state) + except Exception: + state = ArmingState.UNKNOWN + if hasattr(state, 'is_triggered') and state.is_triggered(): + return 0 + if hasattr(state, 'is_disarmed') and state.is_disarmed(): + return 1 + return 2 + + +def generate_html_report(data): + """Genera un archivo HTML premium, limpio, claro y compacto con el estatus consolidado.""" + now = datetime.datetime.now(ZoneInfo("America/Mexico_City")) + dias = ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado", "domingo"] + meses = ["enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"] + + timestamp = f"{dias[now.weekday()]}, {now.day} de {meses[now.month - 1]} de {now.year} - {now.strftime('%I:%M:%S %p')}" + + # Generar checkboxes dinámicos para las cuentas de credentials.json + checkboxes_html = "" + accounts_cfg, _, _, _ = load_credentials_config() + if accounts_cfg: + for acc in accounts_cfg: + label = acc.get("label", "") + checkboxes_html += f""" + """ + + # Consolidar todas las sucursales en una sola lista plana + all_locations = [] + for account in data: + for loc in account['locations']: + all_locations.append({ + "account_label": account['label'], + "id": loc['id'], + "name": loc['name'], + "status": loc['status'], + "triggered_partitions": loc.get('triggered_partitions', []), + "active_zones": loc.get('active_zones', []), + "last_download_time": loc.get('last_download_time'), + "last_update_time": loc.get('last_update_time') + }) + + # Ordenar prioritariamente: + # 0: Alarmas activas (¡ALARMA ACTIVA!) + # 1: Desarmadas (sucursales abiertas) + # 2: Armadas (Stay, Away, etc.) + # 3: Revocadas/Desconectadas (con error de token / caché) + # Dentro de cada grupo se ordena de forma secuencial por número/nombre. + all_locations.sort(key=lambda x: (_get_location_sort_priority(x), x['name'])) + + cards_html = "" + if not all_locations: + cards_html = """ +
+
+

Sin datos de sucursales

+

+ Haz clic en el botón "Actualizar" arriba para conectar a los paneles de alarma por primera vez. +

+
+ """ + else: + for loc in all_locations: + state = parse_arming_state(loc['status']) + + # Generar detalles adicionales si la sucursal está alarmada o tiene zonas abiertas/fallos + details_html = "" + + if loc["triggered_partitions"]: + parts_str = ", ".join(loc["triggered_partitions"]) + details_html += f""" +
+ 🚨 Alarma en: {parts_str} +
""" + + if loc["active_zones"]: + zones_list_html = "" + for z in loc["active_zones"]: + badge_cls = "zone-alarm" if z["status"] == "Alarma" else "zone-open" + + # Formatear la hora de activación si existe y es alarma + time_str = "" + if z["status"] == "Alarma" and z.get("trigger_time"): + time_str = format_trigger_time(z["trigger_time"]) + + zones_list_html += f""" +
+ {z["status"]} + + {z["name"]} (Z{z["id"]}){time_str} + +
""" + + details_html += f""" +
+
Zonas activas / fallos:
+
+ {zones_list_html} +
+
""" + + card_class = "branch-card" + if isinstance(loc['status'], str) and loc['status'] == "REVOKED": + card_class += " branch-card-disabled" + + dl_time = loc.get('last_download_time') or 'Nunca' + up_time = loc.get('last_update_time') or 'Nunca' + footer_html = f""" + + """ + + cards_html += f""" +
+
+ + + {state['label']} + + +
+
🏢 {loc['name']}
+ {details_html} + {footer_html} +
""" + + html_content = f""" + + + + + Estatus de Sucursales - TotalConnect + + + + + + +
+
+
+

TotalConnect 2.0

+
Monitoreo centralizado de alarmas
+
+
+
+ Cuentas: + {checkboxes_html} +
+ + + +
+
Última Actualización
+
{timestamp}
+
+
+
+ +
+ {cards_html} +
+ +
+ Generado automáticamente on-demand • TotalConnect REST Client +
+
+ + +
+
+
+

+ + Log de Actividad del Servidor (Últimas 150 líneas) +

+ +
+
+
Cargando bitácora de actividad...
+
+ +
+
+ + +
+
+
Actualizando Estatus
+
Conectando a los paneles de alarma... Esto tomará aproximadamente 1-2 minutos.
+
+ + + +""" + + with open(HTML_OUTPUT_FILE, 'w', encoding='utf-8') as f: + f.write(html_content) + log_msg(f"🎉 Reporte HTML premium generado con éxito en: {HTML_OUTPUT_FILE}") + + +class DashboardHandler(http.server.SimpleHTTPRequestHandler): + def log_message(self, format, *args): + # Silenciar logs de peticiones de red ordinarias en consola/stderr + pass + + def do_GET(self): + if self.path == '/': + self.send_response(200) + self.send_header('Content-type', 'text/html; charset=utf-8') + self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate') + self.send_header('Pragma', 'no-cache') + self.send_header('Expires', '0') + self.end_headers() + try: + with open(HTML_OUTPUT_FILE, 'r', encoding='utf-8') as f: + self.wfile.write(f.read().encode('utf-8')) + except Exception as e: + self.wfile.write(f"Error cargando el archivo: {e}".encode('utf-8')) + elif self.path == '/activity-log': + self.send_response(200) + self.send_header('Content-type', 'text/plain; charset=utf-8') + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate') + self.end_headers() + try: + if os.path.exists(LOG_FILE): + with open(LOG_FILE, 'r', encoding='utf-8') as f: + lines = f.readlines() + # Devolver las últimas 150 líneas de actividad para la modal + last_lines = "".join(lines[-150:]) + self.wfile.write(last_lines.encode('utf-8')) + else: + self.wfile.write("No hay actividad registrada aún.".encode('utf-8')) + except Exception as e: + self.wfile.write(f"Error al leer activity.log: {e}".encode('utf-8')) + elif self.path == '/current-loading': + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + self.send_header('Access-Control-Allow-Headers', 'Content-Type') + self.end_headers() + self.wfile.write(json.dumps({"status": current_loading_status}).encode('utf-8')) + elif self.path == '/polling-status': + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + self.send_header('Access-Control-Allow-Headers', 'Content-Type') + self.end_headers() + data = { + "enabled": polling_enabled, + "last_poll": last_poll_timestamp, + "loading_status": current_loading_status + } + self.wfile.write(json.dumps(data).encode('utf-8')) + else: + # Comportamiento predeterminado para otros archivos + super().do_GET() + + def do_POST(self): + if self.path == '/refresh': + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + self.send_header('Access-Control-Allow-Headers', 'Content-Type') + self.end_headers() + + # Leer el cuerpo del POST si existe + content_length = int(self.headers.get('Content-Length', 0)) + selected_labels = None + if content_length > 0: + try: + post_data = self.rfile.read(content_length) + body_json = json.loads(post_data.decode('utf-8')) + selected_labels = body_json.get('accounts') + except Exception as e: + print(f"⚠️ Error al parsear cuerpo de solicitud POST: {e}") + + # Intentar adquirir el lock en modo no-bloqueante + acquired = update_lock.acquire(blocking=False) + if not acquired: + self.wfile.write(json.dumps({ + "status": "error", + "message": "Ya hay una actualización en curso en el servidor. Por favor espera a que termine la anterior." + }).encode('utf-8')) + return + + try: + # Cargar configuración completa + accounts_cfg, _, exclude_cfg, _ = load_credentials_config() + + # Filtrar si el usuario seleccionó específicas + if selected_labels is not None: + accounts_to_run = [acc for acc in accounts_cfg if acc.get('label') in selected_labels] + else: + accounts_to_run = accounts_cfg + + success = run_update(accounts=accounts_to_run, exclude=exclude_cfg, full_refresh=True) + if success: + self.wfile.write(json.dumps({"status": "success"}).encode('utf-8')) + else: + self.wfile.write(json.dumps({"status": "error", "message": "No se pudieron recuperar datos de ninguna cuenta."}).encode('utf-8')) + except Exception as e: + self.wfile.write(json.dumps({"status": "error", "message": str(e)}).encode('utf-8')) + finally: + update_lock.release() + elif self.path == '/reset-cache': + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + self.send_header('Access-Control-Allow-Headers', 'Content-Type') + self.end_headers() + + acquired = update_lock.acquire(blocking=False) + if not acquired: + self.wfile.write(json.dumps({ + "status": "error", + "message": "Ya hay una actualización en curso en el servidor. Por favor espera." + }).encode('utf-8')) + return + + try: + # 1. Limpiar sesiones en caliente + with client_pool_lock: + client_pool.clear() + consecutive_errors.clear() + + # 2. Borrar archivos físicos + for fpath in [LAST_STATE_FILE, CACHE_FILE, LAST_EVENT_IDS_FILE]: + if os.path.exists(fpath): + try: + os.remove(fpath) + except Exception: + pass + + log_msg("🗑️ Hard Reset: Se eliminó la caché del servidor a petición del usuario. Iniciando escaneo limpio.") + + # 3. Disparar el poller inmediatamente + polling_trigger_event.set() + + self.wfile.write(json.dumps({ + "status": "success", + "message": "Limpieza de caché completada con éxito. Reiniciando de cero..." + }).encode('utf-8')) + except Exception as e: + self.wfile.write(json.dumps({ + "status": "error", + "message": f"Error al purgar caché: {e}" + }).encode('utf-8')) + finally: + update_lock.release() + elif self.path == '/toggle-polling': + global polling_enabled + polling_enabled = not polling_enabled + save_polling_state(polling_enabled) + state_str = "activado" if polling_enabled else "pausado" + log_msg(f"🛑/🟢 Polling en segundo plano {state_str} por el usuario.") + if polling_enabled: + polling_trigger_event.set() + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + self.send_header('Access-Control-Allow-Headers', 'Content-Type') + self.end_headers() + self.wfile.write(json.dumps({"status": "success", "enabled": polling_enabled}).encode('utf-8')) + else: + self.send_response(404) + self.end_headers() + + def do_OPTIONS(self): + self.send_response(200) + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + self.send_header('Access-Control-Allow-Headers', 'Content-Type') + self.end_headers() + + +class ThreadingHTTPServer(ThreadingMixIn, http.server.HTTPServer): + pass + + +def load_credentials_config(): + """Carga las credenciales, el puerto, la lista de exclusión y la opción de navegador de credentials.json.""" + is_windows = platform.system() == "Windows" + has_display = is_windows or "DISPLAY" in os.environ + default_open = True if has_display else False + + if not os.path.exists(CREDENTIALS_FILE): + return None, 8080, [], default_open + try: + with open(CREDENTIALS_FILE, 'r', encoding='utf-8') as f: + config = json.load(f) + + # Si es un objeto (nuevo formato recomendado) + if isinstance(config, dict): + port = config.get("port", 8080) + exclude = config.get("exclude", []) + open_browser = config.get("open_browser", default_open) + accounts = config.get("accounts", []) + return accounts, port, exclude, open_browser + # Si es una lista (formato heredado compatible) + elif isinstance(config, list): + return config, 8080, [], default_open + except Exception as e: + print(f"⚠️ Error al leer credenciales: {e}") + return None, 8080, [], default_open + return None, 8080, [], default_open + + +def background_poller_thread(): + global polling_enabled, last_poll_timestamp, consecutive_errors + log_msg("🔄 Motor de polling en segundo plano iniciado (intervalo: 30s)...") + while True: + polling_trigger_event.wait(timeout=30) + polling_trigger_event.clear() + + if not polling_enabled: + continue + + acquired = update_lock.acquire(blocking=False) + if not acquired: + continue + + try: + accounts_cfg, _, exclude_cfg, _ = load_credentials_config() + if not accounts_cfg: + continue + + now = datetime.datetime.now(ZoneInfo("America/Mexico_City")) + last_poll_timestamp = now.strftime('%I:%M:%S %p') + + # Comprobar si hay errores persistentes acumulados (Circuit Breaker) + has_persistent_errors = any(err_count >= 3 for err_count in consecutive_errors.values()) + if has_persistent_errors: + log_msg("⚠️ Circuit Breaker: Polling pausado automáticamente por múltiples fallos de autenticación.") + polling_enabled = False + continue + + log_msg(f"🔄 Polling automático iniciado ({last_poll_timestamp})...") + # Ejecutar actualización real de datos desde Honeywell + run_update(accounts=accounts_cfg, exclude=exclude_cfg) + + except Exception as e: + log_msg(f"⚠️ Excepción en hilo de polling: {e}") + finally: + update_lock.release() + + +def run_server(port=8080, open_browser=True): + # Cargar y re-renderizar la plantilla HTML desde last_state.json al arrancar el servidor + if os.path.exists(LAST_STATE_FILE): + try: + with open(LAST_STATE_FILE, 'r', encoding='utf-8') as f: + last_state = json.load(f) + # Reconstruir objetos ArmingState + for acc_state in last_state: + for loc in acc_state.get('locations', []): + if isinstance(loc['status'], int): + try: + loc['status'] = ArmingState(loc['status']) + except Exception: + loc['status'] = ArmingState.UNKNOWN + generate_html_report(last_state) + except Exception as e: + print(f"⚠️ No se pudo regenerar el reporte desde last_state.json: {e}") + elif not os.path.exists(HTML_OUTPUT_FILE): + print("⚠️ No se encontró reporte previo. Generando primer reporte vacío...") + generate_html_report([]) + + # Arrancar hilo en segundo plano para el polling continuo de eventos + poller = threading.Thread(target=background_poller_thread, daemon=True) + poller.start() + + socketserver.TCPServer.allow_reuse_address = True + with ThreadingHTTPServer(("", port), DashboardHandler) as httpd: + url = f"http://localhost:{port}/" + print(f"\n🌐 Servidor de monitoreo iniciado en:") + print(f" - Local: {url}") + print(f" - VM Red: http://192.168.100.50:{port}/") + print("Presiona Ctrl+C para detener el servidor.") + + if open_browser: + print(f" 🚀 Abriendo navegador en {url}...") + try: + webbrowser.open(url) + except Exception as e: + print(f" ⚠️ No se pudo abrir el navegador automáticamente: {e}") + + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\n🛑 Servidor detenido.") + + +def soap_get_events(session_id, last_event_id, location_id=0): + """Consulta nuevos eventos SOAP desde last_event_id. Devuelve (lista_de_eventos, next_event_id).""" + url = "https://rs.alarmnet.com/TC21API/tc2.asmx" + headers = { + 'Content-Type': 'text/xml; charset=utf-8', + 'SOAPAction': 'https://services.alarmnet.com/TC2/GetLiveEvents' + } + + event_count = 50 if last_event_id > 0 else 1 + + soap_body = f""" + + + + {session_id} + {location_id} + {last_event_id} + {event_count} + 1 + 1 + + + """ + + events = [] + max_event_id = last_event_id + + try: + res = requests.post(url, data=soap_body.encode('utf-8'), headers=headers, timeout=12) + res.raise_for_status() + root = ET.fromstring(res.text) + + # Encontrar nodo GetLiveEventsResult o GetSessionUserEventsResult ignorando namespaces + result_node = None + for elem in root.iter(): + if elem.tag.endswith('GetLiveEventsResult') or elem.tag.endswith('GetSessionUserEventsResult'): + result_node = elem + break + + if result_node is None: + result_node = root + + # Extraer ResultCode + result_code_text = "0" + for child in result_node: + if child.tag.endswith('ResultCode'): + result_code_text = child.text + break + + if result_code_text == '0': + # Buscar dinámicamente cualquier nodo que represente un evento por sus campos internos + for elem in result_node.iter(): + event_id_val = None + loc_id_val = None + class_id_val = None + type_id_val = None + + for child in elem: + tag_local = child.tag.split('}')[-1].lower() if '}' in child.tag else child.tag.lower() + if tag_local in ('eventid', 'eventrecordid'): + try: + event_id_val = int(child.text) + except (ValueError, TypeError): + pass + elif tag_local in ('locationid', 'deviceid'): + try: + loc_id_val = int(child.text) + except (ValueError, TypeError): + pass + elif tag_local in ('eventclassid', 'filterclass'): + try: + class_id_val = int(child.text) + except (ValueError, TypeError): + pass + elif tag_local in ('eventtypeid', 'eventtype'): + try: + type_id_val = int(child.text) + except (ValueError, TypeError): + pass + + if event_id_val is not None: + events.append({ + 'event_id': event_id_val, + 'location_id': loc_id_val if loc_id_val is not None else 0, + 'class_id': class_id_val if class_id_val is not None else 0, + 'type_id': type_id_val if type_id_val is not None else 0 + }) + if event_id_val > max_event_id: + max_event_id = event_id_val + elif result_code_text in ('-102', '-103', '4101', '4102'): + raise ConnectionResetError("SOAP Session is invalid or expired.") + else: + # Reportar error en la respuesta + data_str = "" + for child in result_node: + if child.tag.endswith('ResultData'): + data_str = child.text or "" + break + log_msg(f" ⚠️ GetLiveEvents falló con código {result_code_text}: {data_str}") + except ConnectionResetError: + raise + except Exception as e: + log_msg(f" ❌ Error al consultar GetLiveEvents: {e}") + + return events, max_event_id + + +def get_soap_session_id(client, force_refresh=False): + """Obtiene o refresca el SessionID GUID para SOAP usando el cliente REST.""" + username = client.username + if not force_refresh: + guid = soap_session_ids.get(username) + if guid: + return guid + + try: + from total_connect_client.const import HTTP_API_SESSION_DETAILS_ENDPOINT + sd_res = client.http_request( + endpoint=HTTP_API_SESSION_DETAILS_ENDPOINT, + method="GET", + params={"appId": client._app_id, "appVersion": client._app_version} + ) + soap_guid = sd_res.get("SessionDetailsResult", {}).get("SessionID") + if soap_guid: + soap_session_ids[username] = soap_guid + soap_disabled_accounts.discard(username) + return soap_guid + except Exception as e: + log_msg(f" ⚠️ Error al obtener SessionID SOAP para {username}: {e}") + return None + + +def get_authenticated_client(acc, force_reauth=False): + """Obtiene un cliente autenticado reutilizando la sesión activa en memoria si está viva.""" + global client_pool, consecutive_errors + username = acc['username'] + password = acc['password'] + + with client_pool_lock: + if force_reauth: + client_pool.pop(username, None) + + client = client_pool.get(username) + if client and getattr(client, '_logged_in', False): + return client + + print(f"🔑 Autenticando sesión para {acc['label']} ({username})...") + client = TotalConnectClient( + username=username, + password=password, + load_details=False + ) + client_pool[username] = client + consecutive_errors[acc['label']] = 0 + # Extraer SessionID GUID del endpoint sessiondetails para usarlo en SOAP GetLiveEvents + get_soap_session_id(client) + return client + + +def _apply_soap_events(events_list, next_event_id, acc, curr_last_event_id, device_to_location, locations_to_update, log_suffix=""): + """Procesa eventos SOAP recibidos, actualiza locations_to_update y persiste el ID de referencia.""" + suffix_str = f" {log_suffix}" if log_suffix else "" + if curr_last_event_id == 0: + last_event_ids[acc['username']] = next_event_id + save_last_event_ids(last_event_ids) + log_msg(f" ✓ Inicializado rastreador de eventos SOAP{suffix_str} en ID: {next_event_id} (sin procesar eventos previos)") + else: + if events_list: + log_msg(f" 🔔 Detectados {len(events_list)} eventos nuevos en la cuenta {acc['label']}{suffix_str}.") + for ev in events_list: + dev_id_or_loc_id = ev['location_id'] + mapped_loc_id = device_to_location.get(dev_id_or_loc_id, dev_id_or_loc_id) + if mapped_loc_id > 0: + locations_to_update.add(mapped_loc_id) + last_event_ids[acc['username']] = next_event_id + save_last_event_ids(last_event_ids) + + +def run_update(accounts=None, exclude=None, full_refresh=False): + global current_loading_status + current_loading_status = "Iniciando cliente de consulta..." + log_msg("🚀 Iniciando cliente de consulta TotalConnect 2.0 (REST/OAuth2)...") + + try: + if accounts is None or exclude is None: + accounts_cfg, _, exclude_cfg, _ = load_credentials_config() + if accounts is None: + accounts = accounts_cfg + if exclude is None: + exclude = exclude_cfg + + if not accounts: + log_msg(f"❌ No se pudieron cargar las cuentas desde {CREDENTIALS_FILE}") + return False + + # Cargar el último estado consolidado si existe + last_state = [] + if os.path.exists(LAST_STATE_FILE): + try: + with open(LAST_STATE_FILE, 'r', encoding='utf-8') as f: + last_state = json.load(f) + except Exception as e: + log_msg(f"⚠️ No se pudo cargar el último estado: {e}") + + # Reconstruir los objetos ArmingState y mapear sucursales por ID + last_loc_map = {} + for acc_state in last_state: + for loc in acc_state.get('locations', []): + if isinstance(loc['status'], int): + try: + loc['status'] = ArmingState(loc['status']) + except Exception: + loc['status'] = ArmingState.UNKNOWN + last_loc_map[str(loc['id'])] = loc + + last_state_map = {acc_item['label']: acc_item for acc_item in last_state} + updated_labels = set() + + # Cargar caché de ubicaciones conocidas + cache_data = {} + if os.path.exists(CACHE_FILE): + try: + with open(CACHE_FILE, 'r', encoding='utf-8') as f: + cache_data = json.load(f) + except Exception: + cache_data = {} + + report_data = [] + + for acc in accounts: + current_loading_status = f"Conectando a cuenta: {acc['label']}" + log_msg(f"👤 Conectando a cuenta: {acc['label']} ({acc['username']})...") + try: + # Reutilizar sesión autenticada en memoria para velocidad instantánea + try: + client = get_authenticated_client(acc) + except Exception as auth_err: + log_msg(f" ⚠️ Reintentando autenticación limpia para {acc['label']}...") + client = get_authenticated_client(acc, force_reauth=True) + + log_msg(f" ✓ Conexión establecida con éxito. ({len(client.locations)} sucursales)") + + # Construir mapa de device_id a location_id para GetLiveEvents + device_to_location = {} + for loc_id, location in client.locations.items(): + if getattr(location, 'security_device_id', None): + try: + dev_id = int(location.security_device_id) + device_to_location[dev_id] = loc_id + except Exception: + pass + + # Obtener SessionID SOAP si está habilitado + soap_sess_id = None + if acc['username'] not in soap_disabled_accounts: + try: + soap_sess_id = get_soap_session_id(client) + except Exception as soap_init_err: + log_msg(f" ⚠️ SOAP no disponible para {acc['label']}: {soap_init_err}") + soap_disabled_accounts.add(acc['username']) + + # 1. Agrupar sucursales activas (no excluidas) + active_locs_with_obj = [] + for loc_id, location in client.locations.items(): + loc_id_str = str(loc_id) + loc_name_str = location.location_name + if exclude: + if any(str(ex).strip() in loc_id_str or str(ex).strip() in loc_name_str for ex in exclude): + continue + active_locs_with_obj.append((loc_id, location)) + + # Reconstruir mapa de device_id a location_id (si se requiere) + device_to_location = {} + for loc_id, location in active_locs_with_obj: + if getattr(location, 'security_device_id', None): + try: + dev_id = int(location.security_device_id) + device_to_location[dev_id] = loc_id + except Exception: + pass + + # 2. Ordenar por antigüedad de consulta profunda (last_download_epoch) + def get_download_epoch(loc_tuple): + loc_id_str = str(loc_tuple[0]) + prev = last_loc_map.get(loc_id_str, {}) + return prev.get('last_download_epoch', 0) + + active_locs_with_obj.sort(key=get_download_epoch) + + # 3. Construir Grupo A (Secuencial - 27 sucursales) + group_a = active_locs_with_obj[:27] + group_a_ids = {x[0] for x in group_a} + + # 4. Construir Grupo B (Alta Frecuencia - Hasta 7 sucursales) + group_b_pool = [] + for loc_id, location in active_locs_with_obj: + if loc_id in group_a_ids: + continue + loc_id_str = str(loc_id) + prev = last_loc_map.get(loc_id_str, {}) + + status_val = prev.get('status', 0) + if isinstance(status_val, ArmingState): + status_val = status_val.value + + is_disarmed = (status_val == 10200 or str(status_val).upper() == "DISARMED") + is_triggered = len(prev.get('triggered_partitions', [])) > 0 + has_faults = len(prev.get('active_zones', [])) > 0 + + if is_disarmed or is_triggered or has_faults: + group_b_pool.append((loc_id, location)) + + group_b_pool.sort(key=get_download_epoch) + group_b = group_b_pool[:7] + group_b_ids = {x[0] for x in group_b} + + # 5. Construir Grupo C (Triggers de SOAP pendientes) + group_c = [] + for loc_id, location in active_locs_with_obj: + if loc_id in group_a_ids or loc_id in group_b_ids: + continue + loc_id_str = str(loc_id) + prev = last_loc_map.get(loc_id_str, {}) + if prev.get('needs_full_update', False): + group_c.append((loc_id, location)) + + # Lote completo a procesar en este ciclo + lote_completo = group_a + group_b + group_c + lote_ids = {x[0] for x in lote_completo} + + loc_list = [] + cache_locations = {} + full_status_calls = 0 + MAX_FULL_CALLS_PER_CYCLE = 35 + + # Procesar todas las sucursales de la cuenta + for loc_id, location in client.locations.items(): + loc_id_str = str(loc_id) + loc_name_str = location.location_name + if exclude: + if any(str(ex).strip() in loc_id_str or str(ex).strip() in loc_name_str for ex in exclude): + continue + + prev_loc = last_loc_map.get(loc_id_str, {}) + + # Si no está en el lote de este ciclo, re-utilizar la caché de forma instantánea + if loc_id not in lote_ids: + status_val = prev_loc.get('status', ArmingState.UNKNOWN) + loc_list.append({ + "id": loc_id, + "name": loc_name_str, + "status": status_val, + "triggered_partitions": prev_loc.get('triggered_partitions', []), + "active_zones": prev_loc.get('active_zones', []), + "last_download_epoch": prev_loc.get('last_download_epoch', 0), + "last_download_time": prev_loc.get('last_download_time', "Nunca"), + "last_update_epoch": prev_loc.get('last_update_epoch', 0), + "last_update_time": prev_loc.get('last_update_time', "Nunca"), + "needs_full_update": prev_loc.get('needs_full_update', False) + }) + cache_locations[loc_id_str] = loc_name_str + continue + + # Determinar si requiere Poll Full (REST) o Poll Lite (SOAP) + needs_full_update = prev_loc.get('needs_full_update', False) + last_dl_epoch = prev_loc.get('last_download_epoch', 0) + antiguedad_seg = time.time() - last_dl_epoch if last_dl_epoch else 999999 + + reason = None + if full_refresh: + reason = "Refresco total manual solicitado" + elif not prev_loc: + reason = "Arranque en frío (sin caché previa)" + elif needs_full_update: + reason = "Cambio o evento previo detectado por SOAP" + elif antiguedad_seg > 600: + reason = f"Antigüedad de datos superó 10 minutos ({int(antiguedad_seg / 60)} min)" + + # Limitador de velocidad: degradar a SOAP Lite si superamos el tope + if reason and full_status_calls >= MAX_FULL_CALLS_PER_CYCLE: + log_msg(f" ⚠️ Límite de Polls Full por ciclo alcanzado. Degradando '{loc_name_str}' a Poll Lite.") + reason = None + + # --- CASO A: POLL FULL (REST fullStatus) --- + if reason: + full_status_calls += 1 + log_msg(f" ⏳ Poll FULL en '{loc_name_str}' (Motivo: {reason})") + current_loading_status = f"Cargando sucursal: {loc_name_str}" + + try: + endpoint = make_http_endpoint(f"api/v3/locations/{loc_id}/partitions/fullStatus") + result = client.http_request(endpoint=endpoint, method="GET") + client.raise_for_resultcode(result) + + # Actualizar la librería + location._update_status(result) + location._update_partitions(result["PanelStatus"]["Partitions"]) + location._update_zones(result["PanelStatus"]["Zones"]) + + triggered_parts = [] + active_zones = [] + + # Particiones + for p_id, p in location.partitions.items(): + if p.arming_state.is_triggered(): + triggered_parts.append(p.name or f"Partición {p_id}") + + # Zonas + raw_zones = result.get("PanelStatus", {}).get("Zones", []) + raw_zones_map = {int(z["ZoneID"]): z for z in raw_zones if "ZoneID" in z} + + for z_id, z in location.zones.items(): + partition_id = z.partition + partition = location.partitions.get(partition_id) + is_partition_triggered = partition.arming_state.is_triggered() if partition else location.arming_state.is_triggered() + + is_alarm_state = z.is_triggered() and is_partition_triggered + is_fault_state = z.is_faulted() + + if is_alarm_state or is_fault_state: + status_lbl = "Alarma" if is_alarm_state else "Abierta/Fallo" + raw_z = raw_zones_map.get(z_id, {}) + trigger_time = raw_z.get("AlarmTriggerTimeLocalized", "") if is_alarm_state else "" + + active_zones.append({ + "id": z_id, + "name": z.description or f"Zona {z_id}", + "status": status_lbl, + "partition": z.partition, + "trigger_time": trigger_time + }) + + def get_zone_sort_key(z_item): + is_alarm = 0 if z_item["status"] == "Alarma" else 1 + t_time = z_item["trigger_time"] or "9999-99-99T99:99:99" + return (is_alarm, t_time) + + active_zones.sort(key=get_zone_sort_key) + + effective_status = location.arming_state + if hasattr(effective_status, 'is_triggered') and effective_status.is_triggered() and not triggered_parts: + if location.partitions: + first_partition = next(iter(location.partitions.values())) + effective_status = first_partition.arming_state + + now_dt = datetime.datetime.now(ZoneInfo("America/Mexico_City")) + now_str = now_dt.strftime('%d/%m/%Y %I:%M:%S %p') + + # Determinar si el estado físico de armado cambió en este Full + status_changed = False + prev_status_val = prev_loc.get('status') + if isinstance(prev_status_val, ArmingState): + prev_status_val = prev_status_val.value + curr_status_val = effective_status.value if isinstance(effective_status, ArmingState) else effective_status + + if prev_status_val != curr_status_val: + status_changed = True + + last_up_epoch = time.time() if status_changed else prev_loc.get('last_update_epoch', time.time()) + last_up_time = now_str if status_changed else prev_loc.get('last_update_time', now_str) + + loc_list.append({ + "id": loc_id, + "name": loc_name_str, + "status": effective_status, + "triggered_partitions": triggered_parts, + "active_zones": active_zones, + "last_download_epoch": time.time(), + "last_download_time": now_str, + "last_update_epoch": last_up_epoch, + "last_update_time": last_up_time, + "needs_full_update": False + }) + + except Exception as full_err: + log_msg(f" ❌ Falló Poll FULL en '{loc_name_str}': {full_err}. Usando caché.") + loc_list.append({ + "id": loc_id, + "name": loc_name_str, + "status": prev_loc.get('status', ArmingState.UNKNOWN), + "triggered_partitions": prev_loc.get('triggered_partitions', []), + "active_zones": prev_loc.get('active_zones', []), + "last_download_epoch": prev_loc.get('last_download_epoch', 0), + "last_download_time": prev_loc.get('last_download_time', "Nunca"), + "last_update_epoch": prev_loc.get('last_update_epoch', 0), + "last_update_time": prev_loc.get('last_update_time', "Nunca"), + "needs_full_update": prev_loc.get('needs_full_update', False) + }) + + # --- CASO B: POLL LITE (SOAP GetLiveEvents) --- + else: + soap_success = False + if soap_sess_id: + if not isinstance(last_event_ids.get(acc['username']), dict): + last_event_ids[acc['username']] = {} + + curr_last_event_id = last_event_ids[acc['username']].get(str(loc_id), 0) + + try: + events_list, next_event_id = soap_get_events(soap_sess_id, curr_last_event_id, loc_id) + + if curr_last_event_id == 0: + last_event_ids[acc['username']][str(loc_id)] = next_event_id + save_last_event_ids(last_event_ids) + else: + if events_list: + log_msg(f" 🔔 SOAP detectó {len(events_list)} eventos nuevos en '{loc_name_str}'.") + needs_full_update = True + + now_dt = datetime.datetime.now(ZoneInfo("America/Mexico_City")) + now_str = now_dt.strftime('%d/%m/%Y %I:%M:%S %p') + prev_loc['last_update_epoch'] = time.time() + prev_loc['last_update_time'] = now_str + + last_event_ids[acc['username']][str(loc_id)] = next_event_id + save_last_event_ids(last_event_ids) + + soap_success = True + + except ConnectionResetError: + log_msg(f" ⚠️ Sesión SOAP expirada al consultar '{loc_name_str}'. Se reintentará en la siguiente vuelta.") + except Exception as soap_err: + log_msg(f" ⚠️ Error de SOAP en '{loc_name_str}': {soap_err}") + + loc_list.append({ + "id": loc_id, + "name": loc_name_str, + "status": prev_loc.get('status', ArmingState.UNKNOWN), + "triggered_partitions": prev_loc.get('triggered_partitions', []), + "active_zones": prev_loc.get('active_zones', []), + "last_download_epoch": prev_loc.get('last_download_epoch', 0), + "last_download_time": prev_loc.get('last_download_time', "Nunca"), + "last_update_epoch": prev_loc.get('last_update_epoch', 0), + "last_update_time": prev_loc.get('last_update_time', "Nunca"), + "needs_full_update": needs_full_update + }) + + cache_locations[str(loc_id)] = loc_name_str + + cache_data[acc['username']] = { + "label": acc['label'], + "locations": cache_locations + } + + report_data.append({ + "label": acc['label'], + "username": acc['username'], + "locations": loc_list + }) + updated_labels.add(acc['label']) + log_msg(f" ✓ Procesamiento de {acc['label']} finalizado. (Consultas profundas: {full_status_calls})") + + except Exception as e: + log_msg(f" ❌ Error al procesar cuenta {acc['label']}: {e}") + + cached_acc = cache_data.get(acc['username']) + if cached_acc: + log_msg(f" ⚠️ Cargando {len(cached_acc['locations'])} sucursales desde la caché local...") + loc_list = [] + for loc_id, loc_name in cached_acc['locations'].items(): + loc_id_str = str(loc_id) + if exclude: + if any(str(ex).strip() in loc_id_str or str(ex).strip() in loc_name for ex in exclude): + continue + + loc_list.append({ + "id": loc_id, + "name": loc_name, + "status": "REVOKED", + "triggered_partitions": [], + "active_zones": [], + "last_download_epoch": 0, + "last_download_time": "Nunca", + "last_update_epoch": 0, + "last_update_time": "Nunca", + "needs_full_update": False + }) + report_data.append({ + "label": acc['label'], + "username": acc['username'], + "locations": loc_list + }) + updated_labels.add(acc['label']) + + # Intentar persistir la caché de nombres + if cache_data: + try: + with open(CACHE_FILE, 'w', encoding='utf-8') as f: + json.dump(cache_data, f, ensure_ascii=False, indent=2) + except Exception as e: + print(f" ⚠️ No se pudo guardar la caché de ubicaciones: {e}") + + # Mezclar los datos de las cuentas que no se actualizaron en esta ejecución + for label, old_acc_data in last_state_map.items(): + if label not in updated_labels: + report_data.append(old_acc_data) + + # Guardar el estado consolidado actual en last_state.json + serializable_report = [] + for acc_data in report_data: + serializable_locs = [] + for loc in acc_data['locations']: + status_val = loc['status'].value if isinstance(loc['status'], ArmingState) else loc['status'] + serializable_locs.append({ + "id": loc['id'], + "name": loc['name'], + "status": status_val, + "triggered_partitions": loc['triggered_partitions'], + "active_zones": loc['active_zones'], + "last_download_epoch": loc.get('last_download_epoch', 0), + "last_download_time": loc.get('last_download_time', "Nunca"), + "last_update_epoch": loc.get('last_update_epoch', 0), + "last_update_time": loc.get('last_update_time', "Nunca"), + "needs_full_update": loc.get('needs_full_update', False) + }) + serializable_report.append({ + "label": acc_data['label'], + "username": acc_data['username'], + "locations": serializable_locs + }) + + try: + with open(LAST_STATE_FILE, 'w', encoding='utf-8') as f: + json.dump(serializable_report, f, ensure_ascii=False, indent=2) + except Exception as e: + print(f"⚠️ No se pudo guardar el archivo de estado consolidado: {e}") + + if report_data: + generate_html_report(report_data) + return True + else: + print("❌ No se pudieron recuperar datos de ninguna cuenta.") + return False + finally: + current_loading_status = "" + + +def main(): + # Asegurar que la documentación y plantillas existan en la carpeta de ejecución + auto_generate_docs() + + # Cargar credenciales, puerto, exclusiones y opción de navegador + accounts, port, exclude, open_browser = load_credentials_config() + + # Si se pasa --once o -o como argumento, corre una sola vez y sale + if len(sys.argv) > 1 and (sys.argv[1] == '--once' or sys.argv[1] == '-o'): + run_update(accounts, exclude) + else: + # Desactivar navegador si se pasa --no-browser o -nb por CLI + if "--no-browser" in sys.argv or "-nb" in sys.argv: + open_browser = False + + # Modo por defecto: arrancar como servidor HTTP + # Si se especifica un puerto por parámetro de línea de comandos, se prioriza + cmd_port = None + for arg in sys.argv[1:]: + try: + cmd_port = int(arg) + except ValueError: + pass + + final_port = cmd_port if cmd_port is not None else port + run_server(final_port, open_browser) + + +if __name__ == '__main__': + main() diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8ec23ff --- /dev/null +++ b/package-lock.json @@ -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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7bfa4ae --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/print_last_state.py b/print_last_state.py new file mode 100755 index 0000000..f59355f --- /dev/null +++ b/print_last_state.py @@ -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!") diff --git a/wsdl.xml b/wsdl.xml new file mode 100755 index 0000000..d92d30a --- /dev/null +++ b/wsdl.xml @@ -0,0 +1,30788 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Performs Security Panel Quick commands - Arm + + + + + Performs Security Panel Quick Commands - CustomArm + + + + + Retrieves latest custom Arm settings set by the user + + + + + Performs Security Panel Quick commands - Disarm + + + + + Reads 16x2 Alpha Keypad Display and LED Status of Security System + + + + + Establishes Live Connection to Security System + + + + + Sends Keystroke to the Security Panel + + + + + Override Current User's Security Panel Session with his own Previous Session + + + + + Polling API - Which should be called based on Arm/Disarm API output Result Codes + + + + + Polling API - Which should be called based on Arm/Disarm API output Result Codes + + + + + Polling API - Which should be called based on Arm/Disarm API output Result Codes for Multipartion Arming + + + + + Polling API - Which should be called based on Arm/Disarm API output Result Codes for Multipartion Arming + + + + + Returns the current connection status of the security panel + + + + + Return List of Zone Informations for given ZoneId's + + + + + Refresh or Insert a Command for a Security Panel + + + + + Return the Status of security Panel at list of Locations for Given User + + + + + Performs Security Panel Quick commands - Arm + + + + + Performs Security Panel Quick commands - Arm + + + + + Initiates Zone Bypass + + + + + [Partially depreciated in TC2.3+, stuill used for Notifications groups] Get Users that you can Manage + + + + + [Depreciated in TC2.3+] Delete a user + + + + + [Depreciated in TC2.3+] Disable / Enable a user + + + + + Update Eula or SMS Eula for a User + + + + + [Depreciated in TC2.3+] Get User information when editing another user or editing yourself + + + + + [TC2.1+] Replacement for DeleteUserInfo. Deletes a Real User or Marks Virtual user for Deletion + + + + + [TC2.1+] Replacement for DisableEnableUser. Disable / Enable a user + + + + + Deprecated in TC Multilingual - [TC2.1+] Replacement for GetUserInfo. Get User information when editing another user or editing yourself + + + + + [TC2.1+] Replacement for GetUserDetails. Get User information when editing another user or editing yourself-Includes Wifi Lock as well + + + + + [TC3.24 +] Replacement for GetAllUserDetailsEx. It will return user partition related informations + + + + + [TC2.1+] Replacement for GetUserDetails. Get User information when editing another user or editing yourself + + + + + DEPRECATED in TC Multilingual - [TC2.1+] Replacement for SaveCreateUserInfo. Stores User information when editing self/another user or creating a new User + + + + + [TC2.1+] Replacement for SaveCreateUserInfo. Stores User information when editing self/another user or creating a new User + + + + + [TC2.1+] Replacement for SaveUserEx. Stores User information when editing self/another user or creating a new User.Added addtional feature + + + + + [TC3.24+] Replacement for SaveUserEx1. Included Partition informations + + + + + Used to update the usercode after login, so sessionid, oldUsercode, NewUserCode, UserId, LocationID is must + + + + + [TC3.8+] Created for Save MasterUsercode in TC if MasterUsercode changed in the panel. + + + + + [TC2.1+] Replacement for GetUsers. Returns List of Real Users that one can manage if asked at AccountLevel. This also returns Real+Virtual Users if asked at Location Level. + + + + + [TC2.1+] Synchronize Total Connect Users with Security System user codes + + + + + [TC2.1+] Provides list of SMS Email Carriers + + + + + [TC2.2+] Provides list of Notification Methods + + + + + Check if a UserName is ok to use + + + + + [Depreciated in TC2.3+] Add a user to an existing account, save user updates to another user, save user updates to yourself + + + + + Save User-Client Preferences + + + + + Get listing of Help Documents URLs + + + + + Used to update only the Language (LocaleId value) for a User. Country Code (ex. en-US) or a language Id as defined within TC2 can be used to update the value in the User Table. + + + + + Used to update the user password after login, so sessioid, OldPassword and NewPassword is must + + + + + [TC3.12+] Used to get feedback from users + + + + + [TC3.12+] Used to get feedback from users along with EmailID and rating + + + + + Validate user code + + + + + Used to update the user password after login, so sessioid, userName and NewPassword is must + + + + + [TC3.8+] Created for Save MasterUsercode in TC if MasterUsercode changed in the panel. + + + + + Used to update the usercode after login, so sessionid, oldUsercode, NewUserCode, UserId, LocationID is must + + + + + Validate user code + + + + + Remote Request for authentication for the given username/password using the specified client application. + + + + + MAC validation request for the given GUID for Mobile Punch thru access + + + + + Request for authentication for the given username/password using the specified client application. + + + + + Request for authentication for the given username/password using the specified client application. + + + + + Request for authentication for the given GUID using the specified client application. + + + + + Request for authentication for the given GUID using the specified client application. + + + + + Request session details for the given sessionID + + + + + Request for authentication for the given username/password using the specified client application and retrieves the session details on successful authentication + + + + + Request for authentication for the given GUID using the specified client application. + + + + + Request for authentication for the given username/password using the specified client application and retrieves the session details on successful authentication + + + + + Authentication for the given username and password using the specified client application and retrieves the session details on successful authentication + + + + + Request for authentication for the given encrypted username/password using the specified client application and retrieves the session details on successful authentication + + + + + Inquire about an active session for the purpose of avoiding a server-side timeout. + + + + + Request that the given sessionID be logged out and/or terminated. + + + + + Determines anything changed since last update. The result will guide the UI further. NOTE: Deprecated and no longer supported. + + + + + [DEPRECIATED] Request InterfaceSchema details for the given sessionID/username + + + + + Request InterfaceSchema details for the given sessionID/username + + + + + Request InterfaceSchema details for the given sessionID/username + + + + + Request for Static Key information for Clients + + + + + To Remove a single Smart Action + + + + + To Remove the List of Smart Actions + + + + + To Set the Smart Action state + + + + + To Execute the Smart Action + + + + + To Get the Smart Action Configuration + + + + + To Remove a single Smart Action + + + + + To Set the Smart Action state + + + + + To Execute the Smart Action + + + + + Retrieve the current configuration information for the given Video PIR. + + + + + Retrieve the list of Video PIR for the specified session and location. + + + + + Get the capturetypes for VideoPIR device + + + + + Update the configuration information for the given VideoPIR. + + + + + Returns the Video PIR event record based on Event TypeID + + + + + Trigger the Peek In Now Action for VideoPIR. + + + + + Get VideoPIR device events with applied filter condition + + + + + Return a list of WiFi locations and devices. + + + + + Activate WiFi Devices in Total Connect. + + + + + Turn On/Off WiFi Smart Away + + + + + Remove WiFi Device from Total Connect + + + + + This will return the weather forecast for the device location. + + + + + This will return the results for one thermostat. + + + + + Return a list of Lyric locations and devices. + + + + + Activate and Remove Lyric Thermostat in Total Connect. + + + + + Used to Save an image to an object PhotoClassId:1- location, 2 - User image + + + + + Used to Retrieve a Branding Image LOGO + + + + + Returns the data associated with a Location object from the database + + + + + Returns the data associated with the basic attributes of a Location object from the database + + + + + Saves the data associated with a LocationInfo object to the database. Since this method will only update an existing Location, you are required to pass in its LocationId. Pass in a null value for the Image byte array when either the image has not changed or when the default imageIs to be used. Any previous image will be deleted when either a null or actual image is saved. At the curent triggerTime, the Alias is for the one security system associated with this location. + + + + + Saves the data associated with a LocationInfo object to the database. Since this method will only update an existing Location, you are required to pass in its LocationId. Pass in a null value for the Image byte array when either the image has not changed or when the default imageIs to be used. Any previous image will be deleted when either a null or actual image is saved. At the curent triggerTime, the Alias is for the one security system associated with this location. + + + + + Retrieve the list of supported countries + + + + + Retrieve the list of states for a country + + + + + Retrieves the list of countries & States + + + + + Returns the Partners associated with a Location object from the database + + + + + Multiple Skybell Returns the Partners associated with a Location object from the database + + + + + Returns the Partners associated with a Location object from the database + + + + + Multiple Skybell Returns the Partners associated with a Location object from the database + + + + + Subscribe and Unsubscribe Lyric Account + + + + + Returns the Dealer Information + + + + + Returns the Subscriber Information + + + + + Returns the set of available triggerTime zone regions and their ID + + + + + Returns the set of available triggerTime zone regions and their ID for Camera + + + + + Provides version information for the web service. + + + + + Retrieves the set of video file URLs for a device or audit event record + + + + + Retrieves the set of video file URLs for a device or audit event record + + + + + Returns the notes associated with a device or audit event record + + + + + Returns a set of device or audit event records. A maximum of 100 records can be returned per call. + + + + + Returns a set of device or audit event records. A maximum of 100 records can be returned per call. + + + + + Returns doorbell event records. A maximum of 100 records can be returned per call. + + + + + Returns a set of device or audit event records. A maximum of 100 records can be returned per call. + + + + + Returns a set of device or audit event records. A maximum of 100 records can be returned per call.Eevnts by partition + + + + + Returns a list of available event type filter values that are to be used for filtering the event record retrieval. + + + + + Returns a list of available event type filter values that are to be used for filtering the event record retrieval. + + + + + Returns a set of device event records. A maximum of 250 records can be returned per call. The parameter LastEventIdReceived is used to retreive newer records than the ones previously obtained. Entering the most recent event id into this field will return all records that are newer than that record + + + + + Returns a set of device event records. A maximum of 250 records can be returned per call. The parameter LastEventIdReceived is used to retreive newer records than the ones previously obtained. Entering the most recent event id into this field will return all records that are newer than that record + + + + + Returns a set of doorbell event records. A maximum of 250 records can be returned per call. The parameter LastEventIdReceived is used to retreive newer records than the ones previously obtained. Entering the most recent event id into this field will return all records that are newer than that record + + + + + Updates the note associated with a device or audit event. Storing an empty string will clear a note. + + + + + Updates the hidden state of a device or audit event record. Hidden records are not returned during normal GetEvents requests unlessthe ShowHidden parameter is true. + + + + + Updates the lock state of a device or audit event record. When a record is locked, the automated clean up process will retain the locked record. Only unlocked records will be removed. + + + + + Updates the lock state of a device or audit event record. When a record is locked, the automated clean up process will retain the locked record. Only unlocked records will be removed. + + + + + Determine if the camera is available for a stream at the moment. Current reasons for unavailability would be if the camera is updating + + + + + Retrieve the current state (flags, pan/tilt location, WLLED, output state) for the given camera. + + + + + Retrieve the current I/O state (flags, WLLED, output state) for the given camera. + + + + + Retrieve the current pan/tilt location for the given camera. + + + + + Request that the given camera move to the given preset pan/tilt location. + + + + + Request that the current pan/tilt location be stored as the given preset slot for the given camera. + + + + + Request that the given camera move in the specified direction, speed and distance. + + + + + Request that the given camera move to the specified location, based on a 640x480 viewport mapping. + + + + + Request that the WLLED for the given camera be turned on/off. + + + + + Request that the output control for the given camera be turned on/off. + + + + + Request the given camera to archive a video clip to the server. + + + + + Configure the given camera to use the specified video mode. + + + + + Retrieve the list of cameras for the specified session and location. + + + + + Retrieve the list of All cameras for the specified session and location. + + + + + Retrieve the list of All cameras and Multiple Skybell for the specified session and location. + + + + + Retrieve the current configuration information for the given camera. + + + + + Retrieve the current configuration information for the given camera. + + + + + Retrieve the current configuration information (including limits) for the given camera. + + + + + Update the configuration information for the given camera. + + + + + Adjust default camera settings + + + + + Request that the camera firmware be upgraded/changed to the specified version (or default, if unspecified) + + + + + Request a list of available camera firmware upgrade options for the given camera. + + + + + Get camera wireless status + + + + + Get camera Information - provides camera wireless details and Network data + + + + + Gets WAP clients status + + + + + Gets Camera Battery Status + + + + + Get the Event Metadata applicable for the Given Location. Used for Notification Configuration + + + + + Get the Event Metadata applicable for the Given Location. Used for Notification Configuration + + + + + Get the Event Metadata applicable for the Given Location based on Partitions. Used for Notification Configuration + + + + + Get Configured Notification Events at the Given Location. + + + + + Get Configured Notification Events at the Given Location.Added notification flags for each module + + + + + Get Summary of Configured Notification Events in XML at the Given Location. + + + + + Get Summary of Configured Notification Events at the Given Location. + + + + + Update the stored data for the list of event notifications for the given location + + + + + Update the stored data for the list of event notifications for the given location + + + + + Update the stored data for the list of event notifications for the given location + + + + + Get the list of notification states permitted for this location. + + + + + Get the list of notification schedules available. + + + + + Update the stored data for the list of event notification schedules + + + + + Retrieves the Notification Lists associated with current user's TotalConnect account) + + + + + Updates the Notification List associated with the User + + + + + Sends Summary Email about the Notification List to the Admin user + + + + + Enable/disabled notifications for logged in user and/or return notification pause status. + + + + + Get the list of notification states permitted for this location. + + + + + Get the list of notification states permitted for this location. + + + + + [DEPRECATED] Get the Event Metadata applicable for the Given Location. Used for Notification Configuration + + + + + Request for Metadata and Full status of Security Panel for the Given Location and User + + + + + Return the Status of security Panel at multiple Locations for Given User + + + + + Request for Metadata and Full status of Security Panel for the Given Location and User + + + + + Request for Metadata and Full status of Security Panel for the Given Location and User.Added CanBeBypassable flag to determine zone is bypassable or not + + + + + Request for Metadata and Full status of Security Panel for the Given Location and User.Added CanBeBypassable flag to determine zone is bypassable or not + + + + + Request for Full status of Security Panel for the Given Location and User + + + + + Request for Full status of Security Panel for the Given DeviceID and User + + + + + Request for Full status of Security Panel for the Given Location and User + + + + + Request for Full status of Security Panel for the Given Location and User + + + + + Request for Full status of Security Panel for the Given Location and User + + + + + Request for Full status of Security Panel for the Given DeviceID and User + + + + + Get the zones in different states like Alarms, Faults, Trouble, and Bypassed + + + + + [Enriched Method] Get the zones in different states like Alarms, Faults, Trouble, and Bypassed and whether it is Bypassable Zone or Not + + + + + Request for Metadata and Full status of Security Panel for the Given DeviceID and User + + + + + Request for Metadata and Full status of Security Panel for the Given DeviceID and User + + + + + Initiates Zone Bypass for multiple zones + + + + + Initiates Zone Bypass + + + + + Update User's Geofence Area selection - includes Latitude, Longitude and Radius + + + + + Return a Device Invite Token for a Sky Bell User. + + + + + Return Device provisioning status for the given token. + + + + + Update DoorBell Details. + + + + + Removes Doorbell from TC and Skybell + + + + + Removes Doorbell from TC and Skybell for Multiple Skybell per Location + + + + + Get WiFi Door Bell Details. + + + + + Get WiFi Door Bell Details for Multiple Skybell per location. + + + + + Get WiFi Door Bell Settings + + + + + Update WiFi Door Bell Settings + + + + + Get all devices associated to Skybell Account. + + + + + Get Partner Video URL for the device event + + + + + Get DoorBell - Client ID + + + + + Get WiFi Door Bell Diagnostic Details. + + + + + Get WiFi Door Bell Diagnostic Details for Multiple Skybell per Location. + + + + + Get the maximum device count per location for all devices present in that location + + + + + Return Enrollment details for a Partner Camera + + + + + Return Enrollment status for a Partner Camera + + + + + Delete Partner Camera + + + + + Return a Activation status for Lyric Camera. + + + + + Return a Activation status for Lyric Camera. + + + + + Return Access Token detail for App + + + + + Return Refresh Token detail for App + + + + + Return a Settings for Lyric Camera. + + + + + Save the Settings for Lyric Camera. + + + + + Remove Lyric Camera from the Location. + + + + + Return Live Stream detail for Partner Camera + + + + + Return Lyric Camera and RSI Camera status. + + + + + Return Lyric Camera and RSI Camera status. + + + + + Return Live Stream detail for Partner Camera + + + + + Return clip Key detail for App + + + + + Return Live Snapshot detail for Partner Camera + + + + + Save the Settings for Lyric Camera. + + + + + Log Message to Kibana Server + + + + + Return Live Stream detail for Partner Camera + + + + + Save the Settings for VAV Camera. + + + + + Save the Settings for VAV Camera. + + + + + Return List of Partitions for the Given DeviceID + + + + + Return List of Partitions Authority Level info. + + + + + [TC2.1+] R Get User information with Partition Authority + + + + + [Enriched Method] Get the zones in different states like Alarms, Faults, Trouble, and Bypassed and whether it is Bypassable Zone or Not for Multipartition + + + + + Performs Security Panel Quick commands - Arm with specified partitions + + + + + Performs Security Panel Quick commands - Disarm + + + + + Performs Security Panel Quick commands - Arm with specified partitions + + + + + Performs Security Panel Quick commands - Disarm + + + + + configuring enabled feature for each partition-master user + + + + + Stores MobileDeviceToken ID for a User + + + + + Update User's Push notification Subscription + + + + + Remove User's MobileDevice token id + + + + + Pause / Allow all notifications for logged in user + + + + + Get RSI camera wireless, Supervision and Low Battery status + + + + + Look in for RSI + + + + + Set RSI Camera Settings for RSI + + + + + Get RSI Camera Settings for RSI + + + + + Get RSI Video URL Info for the device event + + + + + Return a list of WiFi locations and devices. + + + + + Add/Remoce WiFi Devices in Total Connect. + + + + + Save August Lock OAUTH Access Token in Total Connect. + + + + + Return a list of TCC WiFi locations and devices. + + + + + To Add/Delete the TCC WiFi Thermostats in Total Connect. + + + + + Return Configured TCC WiFi Thermostat Schedules. + + + + + Update the TCC WiFi Thermostat Schedules. + + + + + Success/Failure on Thermostat Access Token Generation + + + + + To Insert the Severe weather test message for the given locationID + + + + + Update a Switch Icon (database only) + + + + + Update a Thermostat schedule (database only) + + + + + Get a Thermostat schedule (from database) + + + + + Execute a single scene + + + + + Create or Edit a single scene + + + + + Delete a single scene + + + + + Get a specific scene per Automation panel (database) + + + + + Get list of scenes per Automation panel (database) + + + + + Get info for all Automation devices per Automation panel (during a panel session from panel, otherwise from database) + + + + + [Transitional State for lock] Get info for all Automation devices per Automation panel (during a panel session from panel, otherwise from database) - Includes HB Thermostat as well + + + + + [Transitional State for lock] Get info for all Automation devices per Automation panel (during a panel session from panel, otherwise from database) + + + + + Get Automation Lock Details. + + + + + Get Device status details will return Lock and security device status + + + + + Execute a action on a Automation panel lock + + + + + Execute a action on a Automation panel thermostat + + + + + Setting Schedule Status for thermostat + + + + + Execute a action on a Automation panel thermostat + + + + + Execute a single action on a Automation panel switch + + + + + Execute Security Panel and Automation Synchronization + + + + + Used to check the status of sync job + + + + + Close Current Security Panel Session + + + + + [Transitional State for lock] Get info for all Automation devices per Automation panel (during a panel session from panel, otherwise from database) - Includes HB Thermostat as well + + + + + [Transitional State for lock] Get info for all Automation devices per Automation panel (during a panel session from panel, otherwise from database) + + + + + Execute a action on a Automation panel thermostat + + + + + Execute Security Panel and Automation Synchronization + + + + + Execute a action on a Automation panel lock + + + + + Retrieve the configuration settings for all the Content + + + + + Save the configuration settings for all the Content + + + + + Retrieve the current weather condition + + + + + Retrieve the five days forecast weather condition + + + + + Retrieve the top 5 dealer messages + + + + + Mark a dealer message as Read + + + + + Retrieve the current weather condition + + + + + Retrieve the five days forecast weather condition + + + + + Used to validate an emailaddress in session + + + + + Used to validate an emailaddress from an external source from an email link + + + + + Used to validate an emailaddress from an external source from an email link + + + + + Used to send an validation link to the user + + + + + Used to send an validation link to the multiple users + + + + + Used to retieve the list of GPS devices associated with a location. + + + + + Used to retrieve the most recent recorded Location related data of the GPS device. + + + + + Used to retrieve the GeoFence information for a particular GPS device + + + + + Used to retrieve the the list of geofences that the GPS device currently is in. + + + + + this will cause the GPS device to return its current location to the database. Retrieval will be from a call to GetGPSDeviceLocationData. + + + + + Updates the Photo associated with a GPS Device and returns the PhotoURL for it. + + + + + Saves the Data for a GPSDevice Only. No Photo alteration will be done. + + + + + Request TWM Session + + + + + To add or Edit the Scenes + + + + + To Retrieve the Scenes information + + + + + To Remove a Scenes + + + + + To Execute the Scenes + + + + + To Get the Smart Action Configuration + + + + + To get the list of EULA's to be signed by the user. + + + + + To get the EULA Text for the given language id and eula id + + + + + To get the EULA Text for the given language id and eula id + + + + + To get the list of All EULA's befor login to be viewed by the user. + + + + + To get the EULA Text for the given language id and eula id + + + + + To update if the device is insecure + + + + + To add or Edit the Smart Action + + + + + To Retrieve the Smart Action information + + + + + + + Performs Security Panel Quick commands - Arm + + + + + Retrieves latest custom Arm settings set by the user + + + + + Performs Security Panel Quick commands - Disarm + + + + + Reads 16x2 Alpha Keypad Display and LED Status of Security System + + + + + Establishes Live Connection to Security System + + + + + Sends Keystroke to the Security Panel + + + + + Override Current User's Security Panel Session with his own Previous Session + + + + + Polling API - Which should be called based on Arm/Disarm API output Result Codes + + + + + Polling API - Which should be called based on Arm/Disarm API output Result Codes + + + + + Returns the current connection status of the security panel + + + + + Refresh or Insert a Command for a Security Panel + + + + + [Partially depreciated in TC2.3+, stuill used for Notifications groups] Get Users that you can Manage + + + + + [Depreciated in TC2.3+] Delete a user + + + + + [Depreciated in TC2.3+] Disable / Enable a user + + + + + Update Eula or SMS Eula for a User + + + + + [Depreciated in TC2.3+] Get User information when editing another user or editing yourself + + + + + [TC2.1+] Replacement for DeleteUserInfo. Deletes a Real User or Marks Virtual user for Deletion + + + + + [TC2.1+] Replacement for DisableEnableUser. Disable / Enable a user + + + + + Deprecated in TC Multilingual - [TC2.1+] Replacement for GetUserInfo. Get User information when editing another user or editing yourself + + + + + [TC2.1+] Replacement for GetUserDetails. Get User information when editing another user or editing yourself-Includes Wifi Lock as well + + + + + [TC3.24 +] Replacement for GetAllUserDetailsEx. It will return user partition related informations + + + + + [TC2.1+] Replacement for GetUserDetails. Get User information when editing another user or editing yourself + + + + + Used to update the usercode after login, so sessionid, oldUsercode, NewUserCode, UserId, LocationID is must + + + + + [TC3.8+] Created for Save MasterUsercode in TC if MasterUsercode changed in the panel. + + + + + [TC2.1+] Replacement for GetUsers. Returns List of Real Users that one can manage if asked at AccountLevel. This also returns Real+Virtual Users if asked at Location Level. + + + + + [TC2.1+] Provides list of SMS Email Carriers + + + + + [TC2.2+] Provides list of Notification Methods + + + + + Save User-Client Preferences + + + + + Get listing of Help Documents URLs + + + + + Used to update only the Language (LocaleId value) for a User. Country Code (ex. en-US) or a language Id as defined within TC2 can be used to update the value in the User Table. + + + + + Used to update the user password after login, so sessioid, OldPassword and NewPassword is must + + + + + [TC3.12+] Used to get feedback from users + + + + + [TC3.12+] Used to get feedback from users along with EmailID and rating + + + + + Validate user code + + + + + Used to update the user password after login, so sessioid, userName and NewPassword is must + + + + + Remote Request for authentication for the given username/password using the specified client application. + + + + + MAC validation request for the given GUID for Mobile Punch thru access + + + + + Request for authentication for the given username/password using the specified client application. + + + + + Request for authentication for the given username/password using the specified client application. + + + + + Request for authentication for the given GUID using the specified client application. + + + + + Request for authentication for the given GUID using the specified client application. + + + + + Request session details for the given sessionID + + + + + Request for authentication for the given username/password using the specified client application and retrieves the session details on successful authentication + + + + + Request for authentication for the given GUID using the specified client application. + + + + + Request for authentication for the given username/password using the specified client application and retrieves the session details on successful authentication + + + + + Authentication for the given username and password using the specified client application and retrieves the session details on successful authentication + + + + + Request for authentication for the given encrypted username/password using the specified client application and retrieves the session details on successful authentication + + + + + Inquire about an active session for the purpose of avoiding a server-side timeout. + + + + + Request that the given sessionID be logged out and/or terminated. + + + + + [DEPRECIATED] Request InterfaceSchema details for the given sessionID/username + + + + + Request InterfaceSchema details for the given sessionID/username + + + + + Request InterfaceSchema details for the given sessionID/username + + + + + Request for Static Key information for Clients + + + + + To Remove a single Smart Action + + + + + To Set the Smart Action state + + + + + To Execute the Smart Action + + + + + To Get the Smart Action Configuration + + + + + Retrieve the current configuration information for the given Video PIR. + + + + + Retrieve the list of Video PIR for the specified session and location. + + + + + Get the capturetypes for VideoPIR device + + + + + Update the configuration information for the given VideoPIR. + + + + + Returns the Video PIR event record based on Event TypeID + + + + + Trigger the Peek In Now Action for VideoPIR. + + + + + Get VideoPIR device events with applied filter condition + + + + + Return a list of WiFi locations and devices. + + + + + Turn On/Off WiFi Smart Away + + + + + Remove WiFi Device from Total Connect + + + + + This will return the weather forecast for the device location. + + + + + This will return the results for one thermostat. + + + + + Return a list of Lyric locations and devices. + + + + + Used to Save an image to an object PhotoClassId:1- location, 2 - User image + + + + + Used to Retrieve a Branding Image LOGO + + + + + Returns the data associated with a Location object from the database + + + + + Returns the data associated with the basic attributes of a Location object from the database + + + + + Retrieve the list of supported countries + + + + + Retrieve the list of states for a country + + + + + Retrieves the list of countries & States + + + + + Returns the Partners associated with a Location object from the database + + + + + Multiple Skybell Returns the Partners associated with a Location object from the database + + + + + Subscribe and Unsubscribe Lyric Account + + + + + Returns the Dealer Information + + + + + Returns the Subscriber Information + + + + + Returns the set of available triggerTime zone regions and their ID + + + + + Returns the set of available triggerTime zone regions and their ID for Camera + + + + + Provides version information for the web service. + + + + + Retrieves the set of video file URLs for a device or audit event record + + + + + Retrieves the set of video file URLs for a device or audit event record + + + + + Returns the notes associated with a device or audit event record + + + + + Returns a set of device or audit event records. A maximum of 100 records can be returned per call. + + + + + Returns a set of device or audit event records. A maximum of 100 records can be returned per call. + + + + + Returns doorbell event records. A maximum of 100 records can be returned per call. + + + + + Returns a list of available event type filter values that are to be used for filtering the event record retrieval. + + + + + Returns a list of available event type filter values that are to be used for filtering the event record retrieval. + + + + + Returns a set of device event records. A maximum of 250 records can be returned per call. The parameter LastEventIdReceived is used to retreive newer records than the ones previously obtained. Entering the most recent event id into this field will return all records that are newer than that record + + + + + Returns a set of device event records. A maximum of 250 records can be returned per call. The parameter LastEventIdReceived is used to retreive newer records than the ones previously obtained. Entering the most recent event id into this field will return all records that are newer than that record + + + + + Returns a set of doorbell event records. A maximum of 250 records can be returned per call. The parameter LastEventIdReceived is used to retreive newer records than the ones previously obtained. Entering the most recent event id into this field will return all records that are newer than that record + + + + + Updates the note associated with a device or audit event. Storing an empty string will clear a note. + + + + + Updates the hidden state of a device or audit event record. Hidden records are not returned during normal GetEvents requests unlessthe ShowHidden parameter is true. + + + + + Updates the lock state of a device or audit event record. When a record is locked, the automated clean up process will retain the locked record. Only unlocked records will be removed. + + + + + Updates the lock state of a device or audit event record. When a record is locked, the automated clean up process will retain the locked record. Only unlocked records will be removed. + + + + + Determine if the camera is available for a stream at the moment. Current reasons for unavailability would be if the camera is updating + + + + + Retrieve the current state (flags, pan/tilt location, WLLED, output state) for the given camera. + + + + + Retrieve the current I/O state (flags, WLLED, output state) for the given camera. + + + + + Retrieve the current pan/tilt location for the given camera. + + + + + Request that the given camera move to the given preset pan/tilt location. + + + + + Request that the current pan/tilt location be stored as the given preset slot for the given camera. + + + + + Request that the given camera move in the specified direction, speed and distance. + + + + + Request that the given camera move to the specified location, based on a 640x480 viewport mapping. + + + + + Request that the WLLED for the given camera be turned on/off. + + + + + Request that the output control for the given camera be turned on/off. + + + + + Request the given camera to archive a video clip to the server. + + + + + Configure the given camera to use the specified video mode. + + + + + Retrieve the list of cameras for the specified session and location. + + + + + Retrieve the list of All cameras for the specified session and location. + + + + + Retrieve the list of All cameras and Multiple Skybell for the specified session and location. + + + + + Retrieve the current configuration information for the given camera. + + + + + Retrieve the current configuration information for the given camera. + + + + + Retrieve the current configuration information (including limits) for the given camera. + + + + + Update the configuration information for the given camera. + + + + + Adjust default camera settings + + + + + Request that the camera firmware be upgraded/changed to the specified version (or default, if unspecified) + + + + + Request a list of available camera firmware upgrade options for the given camera. + + + + + Get camera wireless status + + + + + Get camera Information - provides camera wireless details and Network data + + + + + Gets WAP clients status + + + + + Gets Camera Battery Status + + + + + Get the Event Metadata applicable for the Given Location. Used for Notification Configuration + + + + + Get the Event Metadata applicable for the Given Location. Used for Notification Configuration + + + + + Get the Event Metadata applicable for the Given Location based on Partitions. Used for Notification Configuration + + + + + Get Configured Notification Events at the Given Location. + + + + + Get Configured Notification Events at the Given Location.Added notification flags for each module + + + + + Get Summary of Configured Notification Events in XML at the Given Location. + + + + + Get Summary of Configured Notification Events at the Given Location. + + + + + Get the list of notification states permitted for this location. + + + + + Get the list of notification schedules available. + + + + + Retrieves the Notification Lists associated with current user's TotalConnect account) + + + + + Sends Summary Email about the Notification List to the Admin user + + + + + Enable/disabled notifications for logged in user and/or return notification pause status. + + + + + Get the list of notification states permitted for this location. + + + + + [DEPRECATED] Get the Event Metadata applicable for the Given Location. Used for Notification Configuration + + + + + Request for Metadata and Full status of Security Panel for the Given Location and User + + + + + Request for Metadata and Full status of Security Panel for the Given Location and User + + + + + Request for Metadata and Full status of Security Panel for the Given Location and User.Added CanBeBypassable flag to determine zone is bypassable or not + + + + + Request for Full status of Security Panel for the Given Location and User + + + + + Request for Full status of Security Panel for the Given DeviceID and User + + + + + Request for Full status of Security Panel for the Given Location and User + + + + + Request for Full status of Security Panel for the Given Location and User + + + + + Request for Full status of Security Panel for the Given DeviceID and User + + + + + Get the zones in different states like Alarms, Faults, Trouble, and Bypassed + + + + + [Enriched Method] Get the zones in different states like Alarms, Faults, Trouble, and Bypassed and whether it is Bypassable Zone or Not + + + + + Request for Metadata and Full status of Security Panel for the Given DeviceID and User + + + + + Request for Metadata and Full status of Security Panel for the Given DeviceID and User + + + + + Initiates Zone Bypass for multiple zones + + + + + Initiates Zone Bypass + + + + + Return a Device Invite Token for a Sky Bell User. + + + + + Return Device provisioning status for the given token. + + + + + Removes Doorbell from TC and Skybell + + + + + Removes Doorbell from TC and Skybell for Multiple Skybell per Location + + + + + Get WiFi Door Bell Details. + + + + + Get WiFi Door Bell Details for Multiple Skybell per location. + + + + + Get WiFi Door Bell Settings + + + + + Get all devices associated to Skybell Account. + + + + + Get Partner Video URL for the device event + + + + + Get DoorBell - Client ID + + + + + Get WiFi Door Bell Diagnostic Details. + + + + + Get WiFi Door Bell Diagnostic Details for Multiple Skybell per Location. + + + + + Get the maximum device count per location for all devices present in that location + + + + + Return Enrollment status for a Partner Camera + + + + + Delete Partner Camera + + + + + Return a Activation status for Lyric Camera. + + + + + Return a Activation status for Lyric Camera. + + + + + Return Access Token detail for App + + + + + Return Refresh Token detail for App + + + + + Return a Settings for Lyric Camera. + + + + + Remove Lyric Camera from the Location. + + + + + Return Live Stream detail for Partner Camera + + + + + Return Lyric Camera and RSI Camera status. + + + + + Return Live Stream detail for Partner Camera + + + + + Return clip Key detail for App + + + + + Return Live Snapshot detail for Partner Camera + + + + + Save the Settings for Lyric Camera. + + + + + Log Message to Kibana Server + + + + + Return Live Stream detail for Partner Camera + + + + + Save the Settings for VAV Camera. + + + + + Return List of Partitions Authority Level info. + + + + + [TC2.1+] R Get User information with Partition Authority + + + + + Stores MobileDeviceToken ID for a User + + + + + Update User's Push notification Subscription + + + + + Remove User's MobileDevice token id + + + + + Pause / Allow all notifications for logged in user + + + + + Get RSI camera wireless, Supervision and Low Battery status + + + + + Look in for RSI + + + + + Set RSI Camera Settings for RSI + + + + + Get RSI Camera Settings for RSI + + + + + Get RSI Video URL Info for the device event + + + + + Return a list of WiFi locations and devices. + + + + + Save August Lock OAUTH Access Token in Total Connect. + + + + + Return a list of TCC WiFi locations and devices. + + + + + Return Configured TCC WiFi Thermostat Schedules. + + + + + Success/Failure on Thermostat Access Token Generation + + + + + To Insert the Severe weather test message for the given locationID + + + + + Update a Switch Icon (database only) + + + + + Get a Thermostat schedule (from database) + + + + + Execute a single scene + + + + + Delete a single scene + + + + + Get a specific scene per Automation panel (database) + + + + + Get list of scenes per Automation panel (database) + + + + + Get info for all Automation devices per Automation panel (during a panel session from panel, otherwise from database) + + + + + [Transitional State for lock] Get info for all Automation devices per Automation panel (during a panel session from panel, otherwise from database) - Includes HB Thermostat as well + + + + + [Transitional State for lock] Get info for all Automation devices per Automation panel (during a panel session from panel, otherwise from database) + + + + + Get Automation Lock Details. + + + + + Execute a action on a Automation panel lock + + + + + Execute a action on a Automation panel thermostat + + + + + Setting Schedule Status for thermostat + + + + + Execute a action on a Automation panel thermostat + + + + + Execute a single action on a Automation panel switch + + + + + Execute Security Panel and Automation Synchronization + + + + + Used to check the status of sync job + + + + + Close Current Security Panel Session + + + + + [Transitional State for lock] Get info for all Automation devices per Automation panel (during a panel session from panel, otherwise from database) - Includes HB Thermostat as well + + + + + [Transitional State for lock] Get info for all Automation devices per Automation panel (during a panel session from panel, otherwise from database) + + + + + Retrieve the configuration settings for all the Content + + + + + Retrieve the current weather condition + + + + + Retrieve the five days forecast weather condition + + + + + Retrieve the top 5 dealer messages + + + + + Mark a dealer message as Read + + + + + Retrieve the current weather condition + + + + + Retrieve the five days forecast weather condition + + + + + Used to validate an emailaddress in session + + + + + Used to validate an emailaddress from an external source from an email link + + + + + Used to validate an emailaddress from an external source from an email link + + + + + Used to send an validation link to the user + + + + + Used to send an validation link to the multiple users + + + + + Used to retieve the list of GPS devices associated with a location. + + + + + Used to retrieve the most recent recorded Location related data of the GPS device. + + + + + Used to retrieve the GeoFence information for a particular GPS device + + + + + Used to retrieve the the list of geofences that the GPS device currently is in. + + + + + this will cause the GPS device to return its current location to the database. Retrieval will be from a call to GetGPSDeviceLocationData. + + + + + Updates the Photo associated with a GPS Device and returns the PhotoURL for it. + + + + + Request TWM Session + + + + + To Remove a Scenes + + + + + To Execute the Scenes + + + + + To Get the Smart Action Configuration + + + + + To get the list of EULA's to be signed by the user. + + + + + To get the EULA Text for the given language id and eula id + + + + + To get the EULA Text for the given language id and eula id + + + + + To get the list of All EULA's befor login to be viewed by the user. + + + + + To get the EULA Text for the given language id and eula id + + + + + To update if the device is insecure + + + + + + + Performs Security Panel Quick commands - Arm + + + + + Retrieves latest custom Arm settings set by the user + + + + + Performs Security Panel Quick commands - Disarm + + + + + Reads 16x2 Alpha Keypad Display and LED Status of Security System + + + + + Establishes Live Connection to Security System + + + + + Sends Keystroke to the Security Panel + + + + + Override Current User's Security Panel Session with his own Previous Session + + + + + Polling API - Which should be called based on Arm/Disarm API output Result Codes + + + + + Polling API - Which should be called based on Arm/Disarm API output Result Codes + + + + + Returns the current connection status of the security panel + + + + + Refresh or Insert a Command for a Security Panel + + + + + [Partially depreciated in TC2.3+, stuill used for Notifications groups] Get Users that you can Manage + + + + + [Depreciated in TC2.3+] Delete a user + + + + + [Depreciated in TC2.3+] Disable / Enable a user + + + + + Update Eula or SMS Eula for a User + + + + + [Depreciated in TC2.3+] Get User information when editing another user or editing yourself + + + + + [TC2.1+] Replacement for DeleteUserInfo. Deletes a Real User or Marks Virtual user for Deletion + + + + + [TC2.1+] Replacement for DisableEnableUser. Disable / Enable a user + + + + + Deprecated in TC Multilingual - [TC2.1+] Replacement for GetUserInfo. Get User information when editing another user or editing yourself + + + + + [TC2.1+] Replacement for GetUserDetails. Get User information when editing another user or editing yourself-Includes Wifi Lock as well + + + + + [TC3.24 +] Replacement for GetAllUserDetailsEx. It will return user partition related informations + + + + + [TC2.1+] Replacement for GetUserDetails. Get User information when editing another user or editing yourself + + + + + Used to update the usercode after login, so sessionid, oldUsercode, NewUserCode, UserId, LocationID is must + + + + + [TC3.8+] Created for Save MasterUsercode in TC if MasterUsercode changed in the panel. + + + + + [TC2.1+] Replacement for GetUsers. Returns List of Real Users that one can manage if asked at AccountLevel. This also returns Real+Virtual Users if asked at Location Level. + + + + + [TC2.1+] Provides list of SMS Email Carriers + + + + + [TC2.2+] Provides list of Notification Methods + + + + + Save User-Client Preferences + + + + + Get listing of Help Documents URLs + + + + + Used to update only the Language (LocaleId value) for a User. Country Code (ex. en-US) or a language Id as defined within TC2 can be used to update the value in the User Table. + + + + + Used to update the user password after login, so sessioid, OldPassword and NewPassword is must + + + + + [TC3.12+] Used to get feedback from users + + + + + [TC3.12+] Used to get feedback from users along with EmailID and rating + + + + + Validate user code + + + + + Used to update the user password after login, so sessioid, userName and NewPassword is must + + + + + Remote Request for authentication for the given username/password using the specified client application. + + + + + MAC validation request for the given GUID for Mobile Punch thru access + + + + + Request for authentication for the given username/password using the specified client application. + + + + + Request for authentication for the given username/password using the specified client application. + + + + + Request for authentication for the given GUID using the specified client application. + + + + + Request for authentication for the given GUID using the specified client application. + + + + + Request session details for the given sessionID + + + + + Request for authentication for the given username/password using the specified client application and retrieves the session details on successful authentication + + + + + Request for authentication for the given GUID using the specified client application. + + + + + Request for authentication for the given username/password using the specified client application and retrieves the session details on successful authentication + + + + + Authentication for the given username and password using the specified client application and retrieves the session details on successful authentication + + + + + Request for authentication for the given encrypted username/password using the specified client application and retrieves the session details on successful authentication + + + + + Inquire about an active session for the purpose of avoiding a server-side timeout. + + + + + Request that the given sessionID be logged out and/or terminated. + + + + + [DEPRECIATED] Request InterfaceSchema details for the given sessionID/username + + + + + Request InterfaceSchema details for the given sessionID/username + + + + + Request InterfaceSchema details for the given sessionID/username + + + + + Request for Static Key information for Clients + + + + + To Remove a single Smart Action + + + + + To Set the Smart Action state + + + + + To Execute the Smart Action + + + + + To Get the Smart Action Configuration + + + + + Retrieve the current configuration information for the given Video PIR. + + + + + Retrieve the list of Video PIR for the specified session and location. + + + + + Get the capturetypes for VideoPIR device + + + + + Update the configuration information for the given VideoPIR. + + + + + Returns the Video PIR event record based on Event TypeID + + + + + Trigger the Peek In Now Action for VideoPIR. + + + + + Get VideoPIR device events with applied filter condition + + + + + Return a list of WiFi locations and devices. + + + + + Turn On/Off WiFi Smart Away + + + + + Remove WiFi Device from Total Connect + + + + + This will return the weather forecast for the device location. + + + + + This will return the results for one thermostat. + + + + + Return a list of Lyric locations and devices. + + + + + Used to Save an image to an object PhotoClassId:1- location, 2 - User image + + + + + Used to Retrieve a Branding Image LOGO + + + + + Returns the data associated with a Location object from the database + + + + + Returns the data associated with the basic attributes of a Location object from the database + + + + + Retrieve the list of supported countries + + + + + Retrieve the list of states for a country + + + + + Retrieves the list of countries & States + + + + + Returns the Partners associated with a Location object from the database + + + + + Multiple Skybell Returns the Partners associated with a Location object from the database + + + + + Subscribe and Unsubscribe Lyric Account + + + + + Returns the Dealer Information + + + + + Returns the Subscriber Information + + + + + Returns the set of available triggerTime zone regions and their ID + + + + + Returns the set of available triggerTime zone regions and their ID for Camera + + + + + Provides version information for the web service. + + + + + Retrieves the set of video file URLs for a device or audit event record + + + + + Retrieves the set of video file URLs for a device or audit event record + + + + + Returns the notes associated with a device or audit event record + + + + + Returns a set of device or audit event records. A maximum of 100 records can be returned per call. + + + + + Returns a set of device or audit event records. A maximum of 100 records can be returned per call. + + + + + Returns doorbell event records. A maximum of 100 records can be returned per call. + + + + + Returns a list of available event type filter values that are to be used for filtering the event record retrieval. + + + + + Returns a list of available event type filter values that are to be used for filtering the event record retrieval. + + + + + Returns a set of device event records. A maximum of 250 records can be returned per call. The parameter LastEventIdReceived is used to retreive newer records than the ones previously obtained. Entering the most recent event id into this field will return all records that are newer than that record + + + + + Returns a set of device event records. A maximum of 250 records can be returned per call. The parameter LastEventIdReceived is used to retreive newer records than the ones previously obtained. Entering the most recent event id into this field will return all records that are newer than that record + + + + + Returns a set of doorbell event records. A maximum of 250 records can be returned per call. The parameter LastEventIdReceived is used to retreive newer records than the ones previously obtained. Entering the most recent event id into this field will return all records that are newer than that record + + + + + Updates the note associated with a device or audit event. Storing an empty string will clear a note. + + + + + Updates the hidden state of a device or audit event record. Hidden records are not returned during normal GetEvents requests unlessthe ShowHidden parameter is true. + + + + + Updates the lock state of a device or audit event record. When a record is locked, the automated clean up process will retain the locked record. Only unlocked records will be removed. + + + + + Updates the lock state of a device or audit event record. When a record is locked, the automated clean up process will retain the locked record. Only unlocked records will be removed. + + + + + Determine if the camera is available for a stream at the moment. Current reasons for unavailability would be if the camera is updating + + + + + Retrieve the current state (flags, pan/tilt location, WLLED, output state) for the given camera. + + + + + Retrieve the current I/O state (flags, WLLED, output state) for the given camera. + + + + + Retrieve the current pan/tilt location for the given camera. + + + + + Request that the given camera move to the given preset pan/tilt location. + + + + + Request that the current pan/tilt location be stored as the given preset slot for the given camera. + + + + + Request that the given camera move in the specified direction, speed and distance. + + + + + Request that the given camera move to the specified location, based on a 640x480 viewport mapping. + + + + + Request that the WLLED for the given camera be turned on/off. + + + + + Request that the output control for the given camera be turned on/off. + + + + + Request the given camera to archive a video clip to the server. + + + + + Configure the given camera to use the specified video mode. + + + + + Retrieve the list of cameras for the specified session and location. + + + + + Retrieve the list of All cameras for the specified session and location. + + + + + Retrieve the list of All cameras and Multiple Skybell for the specified session and location. + + + + + Retrieve the current configuration information for the given camera. + + + + + Retrieve the current configuration information for the given camera. + + + + + Retrieve the current configuration information (including limits) for the given camera. + + + + + Update the configuration information for the given camera. + + + + + Adjust default camera settings + + + + + Request that the camera firmware be upgraded/changed to the specified version (or default, if unspecified) + + + + + Request a list of available camera firmware upgrade options for the given camera. + + + + + Get camera wireless status + + + + + Get camera Information - provides camera wireless details and Network data + + + + + Gets WAP clients status + + + + + Gets Camera Battery Status + + + + + Get the Event Metadata applicable for the Given Location. Used for Notification Configuration + + + + + Get the Event Metadata applicable for the Given Location. Used for Notification Configuration + + + + + Get the Event Metadata applicable for the Given Location based on Partitions. Used for Notification Configuration + + + + + Get Configured Notification Events at the Given Location. + + + + + Get Configured Notification Events at the Given Location.Added notification flags for each module + + + + + Get Summary of Configured Notification Events in XML at the Given Location. + + + + + Get Summary of Configured Notification Events at the Given Location. + + + + + Get the list of notification states permitted for this location. + + + + + Get the list of notification schedules available. + + + + + Retrieves the Notification Lists associated with current user's TotalConnect account) + + + + + Sends Summary Email about the Notification List to the Admin user + + + + + Enable/disabled notifications for logged in user and/or return notification pause status. + + + + + Get the list of notification states permitted for this location. + + + + + [DEPRECATED] Get the Event Metadata applicable for the Given Location. Used for Notification Configuration + + + + + Request for Metadata and Full status of Security Panel for the Given Location and User + + + + + Request for Metadata and Full status of Security Panel for the Given Location and User + + + + + Request for Metadata and Full status of Security Panel for the Given Location and User.Added CanBeBypassable flag to determine zone is bypassable or not + + + + + Request for Full status of Security Panel for the Given Location and User + + + + + Request for Full status of Security Panel for the Given DeviceID and User + + + + + Request for Full status of Security Panel for the Given Location and User + + + + + Request for Full status of Security Panel for the Given Location and User + + + + + Request for Full status of Security Panel for the Given DeviceID and User + + + + + Get the zones in different states like Alarms, Faults, Trouble, and Bypassed + + + + + [Enriched Method] Get the zones in different states like Alarms, Faults, Trouble, and Bypassed and whether it is Bypassable Zone or Not + + + + + Request for Metadata and Full status of Security Panel for the Given DeviceID and User + + + + + Request for Metadata and Full status of Security Panel for the Given DeviceID and User + + + + + Initiates Zone Bypass for multiple zones + + + + + Initiates Zone Bypass + + + + + Return a Device Invite Token for a Sky Bell User. + + + + + Return Device provisioning status for the given token. + + + + + Removes Doorbell from TC and Skybell + + + + + Removes Doorbell from TC and Skybell for Multiple Skybell per Location + + + + + Get WiFi Door Bell Details. + + + + + Get WiFi Door Bell Details for Multiple Skybell per location. + + + + + Get WiFi Door Bell Settings + + + + + Get all devices associated to Skybell Account. + + + + + Get Partner Video URL for the device event + + + + + Get DoorBell - Client ID + + + + + Get WiFi Door Bell Diagnostic Details. + + + + + Get WiFi Door Bell Diagnostic Details for Multiple Skybell per Location. + + + + + Get the maximum device count per location for all devices present in that location + + + + + Return Enrollment status for a Partner Camera + + + + + Delete Partner Camera + + + + + Return a Activation status for Lyric Camera. + + + + + Return a Activation status for Lyric Camera. + + + + + Return Access Token detail for App + + + + + Return Refresh Token detail for App + + + + + Return a Settings for Lyric Camera. + + + + + Remove Lyric Camera from the Location. + + + + + Return Live Stream detail for Partner Camera + + + + + Return Lyric Camera and RSI Camera status. + + + + + Return Live Stream detail for Partner Camera + + + + + Return clip Key detail for App + + + + + Return Live Snapshot detail for Partner Camera + + + + + Save the Settings for Lyric Camera. + + + + + Log Message to Kibana Server + + + + + Return Live Stream detail for Partner Camera + + + + + Save the Settings for VAV Camera. + + + + + Return List of Partitions Authority Level info. + + + + + [TC2.1+] R Get User information with Partition Authority + + + + + Stores MobileDeviceToken ID for a User + + + + + Update User's Push notification Subscription + + + + + Remove User's MobileDevice token id + + + + + Pause / Allow all notifications for logged in user + + + + + Get RSI camera wireless, Supervision and Low Battery status + + + + + Look in for RSI + + + + + Set RSI Camera Settings for RSI + + + + + Get RSI Camera Settings for RSI + + + + + Get RSI Video URL Info for the device event + + + + + Return a list of WiFi locations and devices. + + + + + Save August Lock OAUTH Access Token in Total Connect. + + + + + Return a list of TCC WiFi locations and devices. + + + + + Return Configured TCC WiFi Thermostat Schedules. + + + + + Success/Failure on Thermostat Access Token Generation + + + + + To Insert the Severe weather test message for the given locationID + + + + + Update a Switch Icon (database only) + + + + + Get a Thermostat schedule (from database) + + + + + Execute a single scene + + + + + Delete a single scene + + + + + Get a specific scene per Automation panel (database) + + + + + Get list of scenes per Automation panel (database) + + + + + Get info for all Automation devices per Automation panel (during a panel session from panel, otherwise from database) + + + + + [Transitional State for lock] Get info for all Automation devices per Automation panel (during a panel session from panel, otherwise from database) - Includes HB Thermostat as well + + + + + [Transitional State for lock] Get info for all Automation devices per Automation panel (during a panel session from panel, otherwise from database) + + + + + Get Automation Lock Details. + + + + + Execute a action on a Automation panel lock + + + + + Execute a action on a Automation panel thermostat + + + + + Setting Schedule Status for thermostat + + + + + Execute a action on a Automation panel thermostat + + + + + Execute a single action on a Automation panel switch + + + + + Execute Security Panel and Automation Synchronization + + + + + Used to check the status of sync job + + + + + Close Current Security Panel Session + + + + + [Transitional State for lock] Get info for all Automation devices per Automation panel (during a panel session from panel, otherwise from database) - Includes HB Thermostat as well + + + + + [Transitional State for lock] Get info for all Automation devices per Automation panel (during a panel session from panel, otherwise from database) + + + + + Retrieve the configuration settings for all the Content + + + + + Retrieve the current weather condition + + + + + Retrieve the five days forecast weather condition + + + + + Retrieve the top 5 dealer messages + + + + + Mark a dealer message as Read + + + + + Retrieve the current weather condition + + + + + Retrieve the five days forecast weather condition + + + + + Used to validate an emailaddress in session + + + + + Used to validate an emailaddress from an external source from an email link + + + + + Used to validate an emailaddress from an external source from an email link + + + + + Used to send an validation link to the user + + + + + Used to send an validation link to the multiple users + + + + + Used to retieve the list of GPS devices associated with a location. + + + + + Used to retrieve the most recent recorded Location related data of the GPS device. + + + + + Used to retrieve the GeoFence information for a particular GPS device + + + + + Used to retrieve the the list of geofences that the GPS device currently is in. + + + + + this will cause the GPS device to return its current location to the database. Retrieval will be from a call to GetGPSDeviceLocationData. + + + + + Updates the Photo associated with a GPS Device and returns the PhotoURL for it. + + + + + Request TWM Session + + + + + To Remove a Scenes + + + + + To Execute the Scenes + + + + + To Get the Smart Action Configuration + + + + + To get the list of EULA's to be signed by the user. + + + + + To get the EULA Text for the given language id and eula id + + + + + To get the EULA Text for the given language id and eula id + + + + + To get the list of All EULA's befor login to be viewed by the user. + + + + + To get the EULA Text for the given language id and eula id + + + + + To update if the device is insecure + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file