UNPKG

homebridge-adt-pulse

Version:
968 lines (967 loc) 104 kB
import axios from 'axios'; import { wrapper } from 'axios-cookiejar-support'; import { JSDOM } from 'jsdom'; import _ from 'lodash'; import { isErrorLike, serializeError } from 'serialize-error'; import { CookieJar } from 'tough-cookie'; import { detectApiDoSubmitHandlers, detectApiGatewayInformation, detectApiOrbSecurityButtons, detectApiPanelInformation, detectApiPanelStatus, detectApiSensorsInformation, detectApiSensorsStatus, detectGlobalDebugParser, detectGlobalPortalVersion, } from './detect.js'; import { generateFakeDynatracePCHeaderValue, generateFakeReadyButtons, } from './fake.js'; import { paramNetworkId, paramSat, requestPathAccessSignIn, requestPathAccessSignInEXxPartnerAdt, requestPathAccessSignInNetworkIdXxPartnerAdt, requestPathAjaxSyncCheckServTXx, requestPathKeepAlive, requestPathMfaMfaSignInWorkflowChallenge, requestPathQuickControlArmDisarm, requestPathQuickControlServRunRraCommand, requestPathSummarySummary, requestPathSystemDeviceId1, requestPathSystemGateway, requestPathSystemSystem, textPanelEmergencyKeys, textPanelTypeModel, } from './regex.js'; import { debugLog, fetchErrorMessage, fetchMissingSatCode, fetchTableCells, findGatewayManufacturerModel, findNullKeys, findPanelManufacturer, generateHash, isPortalSyncCode, isSessionCleanState, parseArmDisarmMessage, parseDoSubmitHandlers, parseOrbSecurityButtons, parseOrbSensors, parseOrbTextSummary, parseSensorsTable, sleep, stackTracer, } from './utility.js'; export class ADTPulseAPI { #connection; #credentials; #internal; #session; constructor(config, internalConfig) { this.#connection = { subdomain: config.subdomain, }; this.#credentials = { fingerprint: config.fingerprint, password: config.password, username: config.username, }; this.#internal = { baseUrl: internalConfig.baseUrl ?? `https://${this.#connection.subdomain}.adtpulse.com`, debug: internalConfig.debug ?? false, logger: internalConfig.logger ?? null, reportedHashes: [], testMode: { enabled: internalConfig.testMode?.enabled ?? false, isSystemDisarmedBeforeTest: internalConfig.testMode?.isSystemDisarmedBeforeTest ?? false, }, waitTimeAfterArm: 5000, }; this.#session = { backupSatCode: null, httpClient: wrapper(axios.create({ jar: new CookieJar(), validateStatus: () => true, })), isAuthenticated: false, isCleanState: true, networkId: null, portalVersion: null, }; if (config.speed !== 1) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.constructor()', 'warn', `Plugin is now running under ${config.speed}x operational speed. You may see slower device updates`); } switch (config.speed) { case 0.75: this.#internal.waitTimeAfterArm = 6000; break; case 0.5: this.#internal.waitTimeAfterArm = 7000; break; case 0.25: this.#internal.waitTimeAfterArm = 8000; break; default: break; } } } async login() { let errorObject; if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.login()', 'info', `Attempting to login to "${this.#internal.baseUrl}"`); } try { const sessions = {}; if (this.isAuthenticated()) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.login()', 'info', [ 'Already logged in', [ '(', [ `backup sat code: ${this.#session.backupSatCode}`, `network id: ${this.#session.networkId}`, `portal version: ${this.#session.portalVersion}`, ].join(', '), ')', ].join(''), ].join(' ')); } return { action: 'LOGIN', success: true, info: { backupSatCode: this.#session.backupSatCode, networkId: this.#session.networkId, portalVersion: this.#session.portalVersion, }, }; } sessions.axiosIndex = await this.#session.httpClient.get(`${this.#internal.baseUrl}/`, this.getRequestConfig()); if (sessions.axiosIndex.status >= 400) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.login()', 'error', `The remote server responded with a HTTP ${sessions.axiosIndex.status} status code`); } return { action: 'LOGIN', success: false, info: { message: `The remote server responded with a HTTP ${sessions.axiosIndex.status} status code`, }, }; } if (typeof sessions.axiosIndex?.request === 'undefined') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.login()', 'error', 'The HTTP client responded without the "request" object'); } return { action: 'LOGIN', success: false, info: { message: 'The HTTP client responded without the "request" object', }, }; } const axiosIndexRequestPath = sessions.axiosIndex.request.path; const axiosIndexRequestPathValid = requestPathAccessSignIn.test(axiosIndexRequestPath); if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.login()', 'info', `Request path ➜ ${axiosIndexRequestPath}`); debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.login()', 'info', `Request path valid ➜ ${axiosIndexRequestPathValid}`); } if (!axiosIndexRequestPathValid) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.login()', 'error', `"${axiosIndexRequestPath} is not the sign-in page`); } this.handleLoginFailure(axiosIndexRequestPath, sessions.axiosIndex); return { action: 'LOGIN', success: false, info: { message: `"${axiosIndexRequestPath} is not the sign-in page`, }, }; } const loginForm = new URLSearchParams(); loginForm.append('usernameForm', this.#credentials.username); loginForm.append('passwordForm', this.#credentials.password); loginForm.append('sun', 'yes'); loginForm.append('networkid', ''); loginForm.append('fingerprint', this.#credentials.fingerprint); this.#session.portalVersion = axiosIndexRequestPath.replace(requestPathAccessSignIn, '$2'); await this.newInformationDispatcher('portal-version', { version: this.#session.portalVersion }); sessions.axiosSignIn = await this.#session.httpClient.post(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/access/signin.jsp?e=ns&partner=adt`, loginForm, this.getRequestConfig({ headers: { 'Cache-Control': 'max-age=0', 'Content-Type': 'application/x-www-form-urlencoded', Origin: this.#internal.baseUrl, Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/access/signin.jsp?e=ns&partner=adt`, 'Sec-Fetch-Site': 'same-origin', }, })); if (sessions.axiosIndex.status >= 400) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.login()', 'error', `The remote server responded with a HTTP ${sessions.axiosIndex.status} status code`); } return { action: 'LOGIN', success: false, info: { message: `The remote server responded with a HTTP ${sessions.axiosIndex.status} status code`, }, }; } if (typeof sessions.axiosSignIn?.request === 'undefined') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.login()', 'error', 'The HTTP client responded without the "request" object'); } return { action: 'LOGIN', success: false, info: { message: 'The HTTP client responded without the "request" object', }, }; } const axiosSignInRequestPath = sessions.axiosSignIn.request.path; const axiosSignInRequestPathValid = requestPathSummarySummary.test(axiosSignInRequestPath); if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.login()', 'info', `Request path ➜ ${axiosSignInRequestPath}`); debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.login()', 'info', `Request path valid ➜ ${axiosSignInRequestPathValid}`); } if (!axiosSignInRequestPathValid) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.login()', 'error', `"${axiosSignInRequestPath}" is not the summary page`); } this.handleLoginFailure(axiosSignInRequestPath, sessions.axiosSignIn); return { action: 'LOGIN', success: false, info: { message: `"${axiosSignInRequestPath}" is not the summary page`, }, }; } if (typeof sessions.axiosSignIn.data !== 'string') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.login()', 'error', 'The response body of the summary page is not of type "string"'); } return { action: 'LOGIN', success: false, info: { message: 'The response body of the summary page is not of type "string"', }, }; } const matchNetworkId = sessions.axiosSignIn.data.match(paramNetworkId); this.#session.networkId = (matchNetworkId !== null && matchNetworkId.length >= 2) ? matchNetworkId[1] : null; const matchSatCode = sessions.axiosSignIn.data.match(paramSat); this.#session.backupSatCode = (matchSatCode !== null && matchSatCode.length >= 2) ? matchSatCode[1] : null; if (this.#session.backupSatCode === null && this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.login()', 'warn', 'Unable to backup sat code, will try again when system becomes available'); } this.#session.isAuthenticated = true; if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.login()', 'success', [ 'Login successful', [ '(', [ `backup sat code: ${this.#session.backupSatCode}`, `network id: ${this.#session.networkId}`, `portal version: ${this.#session.portalVersion}`, ].join(', '), ')', ].join(''), ].join(' ')); } return { action: 'LOGIN', success: true, info: { backupSatCode: this.#session.backupSatCode, networkId: this.#session.networkId, portalVersion: this.#session.portalVersion, }, }; } catch (error) { errorObject = (isErrorLike(error)) ? serializeError(error) : serializeError(new Error('Unknown error')); } if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.login()', 'error', 'Method encountered an error during execution'); stackTracer('serialize-error', errorObject); } return { action: 'LOGIN', success: false, info: { error: errorObject, }, }; } async logout() { let errorObject; if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.logout()', 'info', `Attempting to logout of "${this.#internal.baseUrl}"`); } try { const sessions = {}; if (!this.isAuthenticated()) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.logout()', 'info', [ 'Already logged out', [ '(', [ `backup sat code: ${this.#session.backupSatCode}`, `network id: ${this.#session.networkId}`, `portal version: ${this.#session.portalVersion}`, ].join(', '), ')', ].join(''), ].join(' ')); } return { action: 'LOGOUT', success: true, info: { backupSatCode: this.#session.backupSatCode, networkId: this.#session.networkId, portalVersion: this.#session.portalVersion, }, }; } sessions.axiosSignout = await this.#session.httpClient.get(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/access/signout.jsp?networkid=${this.#session.networkId}&partner=adt`, this.getRequestConfig({ headers: { Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/summary/summary.jsp`, 'Sec-Fetch-Site': 'same-origin', }, })); if (sessions.axiosSignout.status >= 400) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.logout()', 'error', `The remote server responded with a HTTP ${sessions.axiosSignout.status} status code`); } return { action: 'LOGOUT', success: false, info: { message: `The remote server responded with a HTTP ${sessions.axiosSignout.status} status code`, }, }; } if (typeof sessions.axiosSignout?.request === 'undefined') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.logout()', 'error', 'The HTTP client responded without the "request" object'); } return { action: 'LOGOUT', success: false, info: { message: 'The HTTP client responded without the "request" object', }, }; } const axiosSignoutRequestPath = sessions.axiosSignout.request.path; const axiosSignoutRequestPathValid = requestPathAccessSignInNetworkIdXxPartnerAdt.test(axiosSignoutRequestPath); if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.logout()', 'info', `Request path ➜ ${axiosSignoutRequestPath}`); debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.logout()', 'info', `Request path valid ➜ ${axiosSignoutRequestPathValid}`); } if (!axiosSignoutRequestPathValid) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.logout()', 'error', `"${axiosSignoutRequestPath}" is not the sign-in page with "networkid" and "partner=adt" parameters`); } this.handleLoginFailure(axiosSignoutRequestPath, sessions.axiosSignout); return { action: 'LOGOUT', success: false, info: { message: `"${axiosSignoutRequestPath}" is not the sign-in page with "networkid" and "partner=adt" parameters`, }, }; } this.resetSession(); if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.logout()', 'success', [ 'Logout successful', [ '(', [ `backup sat code: ${this.#session.backupSatCode}`, `network id: ${this.#session.networkId}`, `portal version: ${this.#session.portalVersion}`, ].join(', '), ')', ].join(''), ].join(' ')); } return { action: 'LOGOUT', success: true, info: { backupSatCode: this.#session.backupSatCode, networkId: this.#session.networkId, portalVersion: this.#session.portalVersion, }, }; } catch (error) { errorObject = (isErrorLike(error)) ? serializeError(error) : serializeError(new Error('Unknown error')); } if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.logout()', 'error', 'Method encountered an error during execution'); stackTracer('serialize-error', errorObject); } return { action: 'LOGOUT', success: false, info: { error: errorObject, }, }; } async getGatewayInformation() { let errorObject; if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getGatewayInformation()', 'info', `Attempting to retrieve gateway information from "${this.#internal.baseUrl}"`); } try { const sessions = {}; sessions.axiosSystemGateway = await this.#session.httpClient.get(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/system/gateway.jsp`, this.getRequestConfig({ headers: { Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/system/system.jsp`, 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-User': undefined, }, })); if (sessions.axiosSystemGateway.status >= 400) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getGatewayInformation()', 'error', `The remote server responded with a HTTP ${sessions.axiosSystemGateway.status} status code`); } return { action: 'GET_GATEWAY_INFORMATION', success: false, info: { message: `The remote server responded with a HTTP ${sessions.axiosSystemGateway.status} status code`, }, }; } if (typeof sessions.axiosSystemGateway?.request === 'undefined') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getGatewayInformation()', 'error', 'The HTTP client responded without the "request" object'); } return { action: 'GET_GATEWAY_INFORMATION', success: false, info: { message: 'The HTTP client responded without the "request" object', }, }; } const axiosSystemGatewayRequestPath = sessions.axiosSystemGateway.request.path; const axiosSystemGatewayRequestPathValid = requestPathSystemGateway.test(axiosSystemGatewayRequestPath); if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getGatewayInformation()', 'info', `Request path ➜ ${axiosSystemGatewayRequestPath}`); debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getGatewayInformation()', 'info', `Request path valid ➜ ${axiosSystemGatewayRequestPathValid}`); } if (!axiosSystemGatewayRequestPathValid) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getGatewayInformation()', 'error', `"${axiosSystemGatewayRequestPath}" is not the system gateway page`); } this.handleLoginFailure(axiosSystemGatewayRequestPath, sessions.axiosSystemGateway); return { action: 'GET_GATEWAY_INFORMATION', success: false, info: { message: `"${axiosSystemGatewayRequestPath}" is not the system gateway page`, }, }; } if (typeof sessions.axiosSystemGateway.data !== 'string') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getGatewayInformation()', 'error', 'The response body of the system gateway page is not of type "string"'); } return { action: 'GET_GATEWAY_INFORMATION', success: false, info: { message: 'The response body of the system gateway page is not of type "string"', }, }; } sessions.jsdomSystemGateway = new JSDOM(sessions.axiosSystemGateway.data, { url: sessions.axiosSystemGateway.config.url, referrer: sessions.axiosSystemGateway.config.headers.Referer, contentType: 'text/html', pretendToBeVisual: true, }); const jsdomSystemGatewayTableCells = sessions.jsdomSystemGateway.window.document.querySelectorAll('td'); const fetchedTableCells = fetchTableCells(jsdomSystemGatewayTableCells, [ 'Broadband Connection Status:', 'Broadband LAN IP Address:', 'Broadband LAN MAC:', 'Cellular Connection Status:', 'Cellular Signal Strength:', 'Device LAN IP Address:', 'Device LAN MAC:', 'Firmware Version:', 'Hardware Version:', 'Last Update:', 'Manufacturer:', 'Model:', 'Next Update:', 'Primary Connection Type:', 'Router LAN IP Address:', 'Router WAN IP Address:', 'Serial Number:', 'Status:', ], 1, 1); const manufacturer = _.get(fetchedTableCells, ['Manufacturer:', 0], null); const model = _.get(fetchedTableCells, ['Model:', 0], null); const parsedManufacturer = findGatewayManufacturerModel('manufacturer', manufacturer, model); const parsedModel = findGatewayManufacturerModel('model', manufacturer, model); const gatewayInformation = { communication: { broadbandConnectionStatus: _.get(fetchedTableCells, ['Broadband Connection Status:', 0], null), cellularConnectionStatus: _.get(fetchedTableCells, ['Cellular Connection Status:', 0], null), cellularSignalStrength: _.get(fetchedTableCells, ['Cellular Signal Strength:', 0], null), primaryConnectionType: _.get(fetchedTableCells, ['Primary Connection Type:', 0], null), }, manufacturer: parsedManufacturer, model: parsedModel, network: { broadband: { ip: _.get(fetchedTableCells, ['Broadband LAN IP Address:', 0], null), mac: _.get(fetchedTableCells, ['Broadband LAN MAC:', 0], null), }, device: { ip: _.get(fetchedTableCells, ['Device LAN IP Address:', 0], null), mac: _.get(fetchedTableCells, ['Device LAN MAC:', 0], null), }, router: { lanIp: _.get(fetchedTableCells, ['Router LAN IP Address:', 0], null), wanIp: _.get(fetchedTableCells, ['Router WAN IP Address:', 0], null), }, }, serialNumber: _.get(fetchedTableCells, ['Serial Number:', 0], null), status: _.get(fetchedTableCells, ['Status:', 0], null), update: { last: _.get(fetchedTableCells, ['Last Update:', 0], null), next: _.get(fetchedTableCells, ['Next Update:', 0], null), }, versions: { firmware: _.get(fetchedTableCells, ['Firmware Version:', 0], null), hardware: _.get(fetchedTableCells, ['Hardware Version:', 0], null), }, }; await this.newInformationDispatcher('gateway-information', gatewayInformation); await this.newInformationDispatcher('debug-parser', { method: 'getGatewayInformation', response: fetchedTableCells, rawHtml: sessions.axiosSystemGateway.data, }); if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getGatewayInformation()', 'success', `Successfully retrieved gateway information from "${this.#internal.baseUrl}"`); } return { action: 'GET_GATEWAY_INFORMATION', success: true, info: gatewayInformation, }; } catch (error) { errorObject = (isErrorLike(error)) ? serializeError(error) : serializeError(new Error('Unknown error')); } if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getGatewayInformation()', 'error', 'Method encountered an error during execution'); stackTracer('serialize-error', errorObject); } return { action: 'GET_GATEWAY_INFORMATION', success: false, info: { error: errorObject, }, }; } async getPanelInformation() { let errorObject; if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getPanelInformation()', 'info', `Attempting to retrieve panel information from "${this.#internal.baseUrl}"`); } try { const sessions = {}; sessions.axiosSystemDeviceId1 = await this.#session.httpClient.get(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/system/device.jsp?id=1`, this.getRequestConfig({ headers: { Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/system/system.jsp`, 'Sec-Fetch-Site': 'same-origin', }, })); if (sessions.axiosSystemDeviceId1.status >= 400) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getPanelInformation()', 'error', `The remote server responded with a HTTP ${sessions.axiosSystemDeviceId1.status} status code`); } return { action: 'GET_PANEL_INFORMATION', success: false, info: { message: `The remote server responded with a HTTP ${sessions.axiosSystemDeviceId1.status} status code`, }, }; } if (typeof sessions.axiosSystemDeviceId1?.request === 'undefined') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getPanelInformation()', 'error', 'The HTTP client responded without the "request" object'); } return { action: 'GET_PANEL_INFORMATION', success: false, info: { message: 'The HTTP client responded without the "request" object', }, }; } const axiosSystemDeviceId1RequestPath = sessions.axiosSystemDeviceId1.request.path; const axiosSystemDeviceId1RequestPathValid = requestPathSystemDeviceId1.test(axiosSystemDeviceId1RequestPath); if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getPanelInformation()', 'info', `Request path ➜ ${axiosSystemDeviceId1RequestPath}`); debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getPanelInformation()', 'info', `Request path valid ➜ ${axiosSystemDeviceId1RequestPathValid}`); } if (!axiosSystemDeviceId1RequestPathValid) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getPanelInformation()', 'error', `"${axiosSystemDeviceId1RequestPath}" is not the system device id 1 page`); } this.handleLoginFailure(axiosSystemDeviceId1RequestPath, sessions.axiosSystemDeviceId1); return { action: 'GET_PANEL_INFORMATION', success: false, info: { message: `"${axiosSystemDeviceId1RequestPath}" is not the system device id 1 page`, }, }; } if (typeof sessions.axiosSystemDeviceId1.data !== 'string') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getPanelInformation()', 'error', 'The response body of the system device id 1 page is not of type "string"'); } return { action: 'GET_PANEL_INFORMATION', success: false, info: { message: 'The response body of the system device id 1 page is not of type "string"', }, }; } sessions.jsdomSystemDeviceId1 = new JSDOM(sessions.axiosSystemDeviceId1.data, { url: sessions.axiosSystemDeviceId1.config.url, referrer: sessions.axiosSystemDeviceId1.config.headers.Referer, contentType: 'text/html', pretendToBeVisual: true, }); const jsdomSystemDeviceId1TableCells = sessions.jsdomSystemDeviceId1.window.document.querySelectorAll('td'); const fetchedTableCells = fetchTableCells(jsdomSystemDeviceId1TableCells, [ 'Emergency Keys:', 'Manufacturer/Provider:', 'Security Panel Master Code:', 'Status:', 'Type/Model:', ], 1, 1); const emergencyKeys = _.get(fetchedTableCells, ['Emergency Keys:', 0], null); const manufacturerProvider = _.get(fetchedTableCells, ['Manufacturer/Provider:', 0], null); const typeModel = _.get(fetchedTableCells, ['Type/Model:', 0], null); const parsedEmergencyKeys = (emergencyKeys !== null) ? emergencyKeys.match(textPanelEmergencyKeys) : null; const parsedManufacturer = findPanelManufacturer(manufacturerProvider, typeModel); const parsedType = (typeModel !== null && typeModel.includes(' - ')) ? typeModel.replace(textPanelTypeModel, '$1') : null; const parsedModel = (typeModel !== null) ? typeModel.replace(textPanelTypeModel, '$2') : null; const panelInformation = { emergencyKeys: parsedEmergencyKeys, manufacturer: parsedManufacturer, masterCode: _.get(fetchedTableCells, ['Security Panel Master Code:', 0], null), provider: 'ADT', type: parsedType, model: parsedModel, status: _.get(fetchedTableCells, ['Status:', 0], null), }; await this.newInformationDispatcher('panel-information', panelInformation); await this.newInformationDispatcher('debug-parser', { method: 'getPanelInformation', response: fetchedTableCells, rawHtml: sessions.axiosSystemDeviceId1.data, }); if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getPanelInformation()', 'success', `Successfully retrieved panel information from "${this.#internal.baseUrl}"`); } return { action: 'GET_PANEL_INFORMATION', success: true, info: panelInformation, }; } catch (error) { errorObject = (isErrorLike(error)) ? serializeError(error) : serializeError(new Error('Unknown error')); } if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getPanelInformation()', 'error', 'Method encountered an error during execution'); stackTracer('serialize-error', errorObject); } return { action: 'GET_PANEL_INFORMATION', success: false, info: { error: errorObject, }, }; } async getPanelStatus() { let errorObject; if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getPanelStatus()', 'info', `Attempting to retrieve panel status from "${this.#internal.baseUrl}"`); } try { const sessions = {}; sessions.axiosSummary = await this.#session.httpClient.get(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/summary/summary.jsp`, this.getRequestConfig({ headers: { Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/summary/summary.jsp`, 'Sec-Fetch-Site': 'same-origin', }, })); if (sessions.axiosSummary.status >= 400) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getPanelStatus()', 'error', `The remote server responded with a HTTP ${sessions.axiosSummary.status} status code`); } return { action: 'GET_PANEL_STATUS', success: false, info: { message: `The remote server responded with a HTTP ${sessions.axiosSummary.status} status code`, }, }; } if (typeof sessions.axiosSummary?.request === 'undefined') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getPanelStatus()', 'error', 'The HTTP client responded without the "request" object'); } return { action: 'GET_PANEL_STATUS', success: false, info: { message: 'The HTTP client responded without the "request" object', }, }; } const axiosSummaryRequestPath = sessions.axiosSummary.request.path; const axiosSummaryRequestPathValid = requestPathSummarySummary.test(axiosSummaryRequestPath); if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getPanelStatus()', 'info', `Request path ➜ ${axiosSummaryRequestPath}`); debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getPanelStatus()', 'info', `Request path valid ➜ ${axiosSummaryRequestPathValid}`); } if (!axiosSummaryRequestPathValid) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getPanelStatus()', 'error', `"${axiosSummaryRequestPath}" is not the summary page`); } this.handleLoginFailure(axiosSummaryRequestPath, sessions.axiosSummary); return { action: 'GET_PANEL_STATUS', success: false, info: { message: `"${axiosSummaryRequestPath}" is not the summary page`, }, }; } if (typeof sessions.axiosSummary.data !== 'string') { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getPanelStatus()', 'error', 'The response body of the summary page is not of type "string"'); } return { action: 'GET_PANEL_STATUS', success: false, info: { message: 'The response body of the summary page is not of type "string"', }, }; } if (this.#session.backupSatCode === null) { const missingSatCode = fetchMissingSatCode(sessions.axiosSummary); if (missingSatCode !== null) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getPanelStatus()', 'success', 'Backup sat code was successfully recovered from previous failed retrieval'); } this.#session.backupSatCode = missingSatCode; } } sessions.jsdomSummary = new JSDOM(sessions.axiosSummary.data, { url: sessions.axiosSummary.config.url, referrer: sessions.axiosSummary.config.headers.Referer, contentType: 'text/html', pretendToBeVisual: true, }); const jsdomSummaryOrbTextSummary = sessions.jsdomSummary.window.document.querySelector('#divOrbTextSummary'); const parsedOrbTextSummary = parseOrbTextSummary(jsdomSummaryOrbTextSummary); await this.newInformationDispatcher('panel-status', parsedOrbTextSummary); await this.newInformationDispatcher('debug-parser', { method: 'getPanelStatus', response: parsedOrbTextSummary, rawHtml: sessions.axiosSummary.data, }); if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getPanelStatus()', 'success', `Successfully retrieved panel status from "${this.#internal.baseUrl}"`); } return { action: 'GET_PANEL_STATUS', success: true, info: parsedOrbTextSummary, }; } catch (error) { errorObject = (isErrorLike(error)) ? serializeError(error) : serializeError(new Error('Unknown error')); } if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.getPanelStatus()', 'error', 'Method encountered an error during execution'); stackTracer('serialize-error', errorObject); } return { action: 'GET_PANEL_STATUS', success: false, info: { error: errorObject, }, }; } async setPanelStatus(armFrom, armTo, isAlarmActive) { let errorObject; if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.setPanelStatus()', 'info', `Attempting to update panel status from "${armFrom}" to "${armTo}" at "${this.#internal.baseUrl}"`); } if (armFrom !== 'away' && armFrom !== 'night' && armFrom !== 'off' && armFrom !== 'stay') { return { action: 'SET_PANEL_STATUS', success: false, info: { message: `"${armFrom}" is an invalid arm from state`, }, }; } if (armTo !== 'away' && armTo !== 'night' && armTo !== 'off' && armTo !== 'stay') { return { action: 'SET_PANEL_STATUS', success: false, info: { message: `"${armTo}" is an invalid arm to state`, }, }; } if (typeof isAlarmActive !== 'boolean') { return { action: 'SET_PANEL_STATUS', success: false, info: { message: 'You must specify if the system\'s alarm is currently ringing (true) or not (false)', }, }; } if ((armFrom === 'away' && armTo === 'away') || (armFrom === 'night' && armTo === 'night') || (armFrom === 'stay' && armTo === 'stay') || (armFrom === 'off' && armTo === 'off' && !isAlarmActive)) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.setPanelStatus()', 'info', `No need to change arm state from "${armFrom}" to "${armTo}" due to its equivalence`); } return { action: 'SET_PANEL_STATUS', success: true, info: { forceArmRequired: false, }, }; } if (this.#internal.debug && isAlarmActive) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.setPanelStatus()', 'warn', `Alarm is currently ringing and arm state is being changed from "${armFrom}" to "${armTo}"`); } try { let isAlarmCurrentlyActive = isAlarmActive; const securityButtonsResponse = await this.getOrbSecurityButtons(); if (!securityButtonsResponse.success) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.setPanelStatus()', 'error', 'An error occurred while retrieving security buttons'); } return { action: 'SET_PANEL_STATUS', success: false, info: securityButtonsResponse.info, }; } const securityButtons = securityButtonsResponse.info; let readyButtons = securityButtons.filter((securityButton) => !securityButton.buttonDisabled); if (readyButtons.length === 0 && this.#session.backupSatCode !== null) { readyButtons = generateFakeReadyButtons(securityButtons, this.#session.isCleanState, { relativeUrl: 'quickcontrol/armDisarm.jsp', href: 'rest/adt/ui/client/security/setArmState', sat: this.#session.backupSatCode, }); if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.setPanelStatus()', 'warn', 'No security buttons were found. Replacing stuck orb security buttons with fake buttons'); stackTracer('fake-ready-buttons', { before: securityButtons, after: readyButtons, }); } } if (readyButtons.length === 0 && this.#session.backupSatCode === null) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.setPanelStatus()', 'error', 'No security buttons were found and replacement failed because no backup sat code exists'); } return { action: 'SET_PANEL_STATUS', success: false, info: { message: 'No security buttons were found and replacement failed because no backup sat code exists', }, }; } if (readyButtons.length < 1) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.setPanelStatus()', 'error', 'Security buttons are not found on the summary page'); } return { action: 'SET_PANEL_STATUS', success: false, info: { message: 'Security buttons are not found on the summary page', }, }; } if (this.#internal.testMode.enabled && !this.#internal.testMode.isSystemDisarmedBeforeTest) { if (!['off', 'disarmed'].includes(readyButtons[0].urlParams.armState)) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.setPanelStatus()', 'error', 'Test mode is active and system is not disarmed'); } return { action: 'SET_PANEL_STATUS', success: false, info: { message: 'Test mode is active and system is not disarmed', }, }; } this.#internal.testMode.isSystemDisarmedBeforeTest = true; } while (isAlarmCurrentlyActive || !['off', 'disarmed'].includes(readyButtons[0].urlParams.armState)) { const armDisarmResponse = await this.armDisarmHandler(isAlarmCurrentlyActive, { relativeUrl: readyButtons[0].relativeUrl, href: readyButtons[0].urlParams.href, armState: readyButtons[0].urlParams.armState, arm: 'off', sat: readyButtons[0].urlParams.sat, }); if (!armDisarmResponse.success) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.setPanelStatus()', 'error', 'An error occurred in the arm disarm handler (while disarming)'); } return { action: 'SET_PANEL_STATUS', success: false, info: armDisarmResponse.info, }; } if (armDisarmResponse.info.readyButtons.length < 1) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.setPanelStatus()', 'error', 'Arm disarm handler failed to find new security buttons'); } return { action: 'SET_PANEL_STATUS', success: false, info: { message: 'Arm disarm handler failed to find new security buttons', }, }; } readyButtons = armDisarmResponse.info.readyButtons; if (isAlarmCurrentlyActive) { isAlarmCurrentlyActive = false; } } let forceArmRequired = false; if (armTo !== 'off') { const armDisarmResponse = await this.armDisarmHandler(false, { relativeUrl: readyButtons[0].relativeUrl, href: readyButtons[0].urlParams.href, armState: readyButtons[0].urlParams.armState, arm: armTo, sat: readyButtons[0].urlParams.sat, }); if (!armDisarmResponse.success) { if (this.#internal.debug) { debugLog(this.#internal.logger, 'api.ts / ADTPulseAPI.setPanelStatus()', 'error', 'An error occurred in the arm disarm handler (while arming)'); } return {