homebridge-adt-pulse
Version:
Homebridge security system platform for ADT Pulse
968 lines (967 loc) • 104 kB
JavaScript
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 {