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}
Cuenta Sucursal / Locación Estatus Alarma
`; 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); });