homebridge-caddx-interlogix
Version:
A homebridge plugin for integrating the ComNav/NetworX/CaddX NX-595E network module with HomeKit
769 lines • 36.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NX595ESecuritySystem = exports.retryDelayDuration = void 0;
const Utilities = __importStar(require("./utility"));
const axios_1 = __importDefault(require("axios"));
const fast_xml_parser_1 = require("fast-xml-parser");
const async_mutex_1 = require("async-mutex");
const definitions_1 = require("./definitions");
exports.retryDelayDuration = 3000;
class NX595ESecuritySystem {
constructor(address, userid, pin, log, useHTTPS = false) {
this.lock = new async_mutex_1.Mutex();
this.sessionID = '';
this.vendor = definitions_1.Vendor.UNDEFINED;
this.version = '';
this.release = '';
this._isMaster = false;
this._isInstaller = false;
this.lastUpdate = new Date();
this.areas = [];
this.zones = [];
this.outputs = [];
this.zoneNameCount = 0;
this.__extra_area_status = [];
this.zonesSequence = [];
this.zonesBank = [];
this._zvbank = [];
this.IPAddress = address;
this.username = userid;
this.passcode = pin;
this.log = log;
this.httpPrefix = (useHTTPS) ? 'https://' : 'http://';
this.client = axios_1.default.create({
baseURL: this.httpPrefix + this.IPAddress,
timeout: 10000,
method: "post",
headers: {
'content-type': 'application/x-www-form-urlencoded'
},
// Makes sure that 30x status codes are not considered errors so that we
// cand handle them properly when making network calls, but at the same
// time forbid the axios library from automatically following redirects
maxRedirects: 0,
validateStatus: function (status) {
return (status >= 200 && status < 400);
}
});
this.client.interceptors.response.use(response => response, error => {
// We really want to throw the error so it is handled and we don't get
// an unhandledrejection error. By throwing here, we are handling the
// rejection, and bubbling up to the closest error handler (try/catch or
// catch method call on a promise).
throw error;
});
}
async login() {
var _a, _b;
// Parameter checks before logging in
if (!(Utilities.checkAddress(this.IPAddress))) {
throw new Error('Not a valid IP address or hostname');
}
if (typeof this.username == 'undefined' || this.username == "") {
throw new Error('Did not specify a username');
}
if (typeof this.passcode == 'undefined' || this.passcode == "") {
throw new Error('Did not specify a user PIN');
}
// Attempting login
this.log.debug('Attempting login...');
const payload = { 'lgname': this.username, 'lgpin': this.passcode };
try {
// Lock the session ID mutex; we are attempting to log in, so we can't
// have requests being made before acquiring a new session
const response = await this.makeRequest('login.cgi', payload, false);
if (response == undefined)
throw new Error("Login unsuccessful");
const data = response.data;
// Login confirmed, parsing session ID
const session = (_a = data.match(/getSession\(\)\{return \"([A-Z0-9]*)\"/)) === null || _a === void 0 ? void 0 : _a[1];
this.sessionID = session;
this.log.debug("Session ID: ", this.sessionID);
// Parsing panel vendor and software details
const vendor = (_b = data.match(/script src=\"\/([a-zA-Z0-9._-]+)\/.*\.js/)) === null || _b === void 0 ? void 0 : _b[1];
const vendorDetails = vendor.split(/[/_-]/);
switch (vendorDetails[1]) {
case "CN": {
this.vendor = definitions_1.Vendor.COMNAV;
break;
}
default: {
throw new Error("Unrecognized vendor");
}
}
this.version = vendorDetails[2];
this.release = vendorDetails[3];
this.log.debug("Vendor Details: ", this.vendor, this.version, this.release);
this.lastUpdate = new Date();
this.log.debug('Logged in successfully.');
// Our new session is acquired and the session ID is stored, so we can
// unlock the mutex; the next function calls rely on network requests,
// so if the mutex stays locked we are heading straight into a deadlock
// Start retrieving area and zone details; pass through the initial Response
await this.retrieveAreas(response);
await this.retrieveZones();
await this.retrieveOutputs();
}
catch (error) {
this.sessionID = "";
// Make sure to unlock the mutex to avoid potential deadlocks
throw (error);
}
}
async logout() {
if (this.sessionID === "")
return;
// Logout gracefully
this.sessionID = "";
await this.makeRequest('logout.cgi', {}, false).catch(() => { });
this.log.debug('Logged out.');
}
async sendAreaCommand(command = definitions_1.SecuritySystemAreaCommand.AREA_CHIME_TOGGLE, areas = []) {
try {
if (this.sessionID === "") {
await this.login();
if (this.sessionID === "")
throw (new Error('Could not send area command; not logged in'));
}
if (!(command in definitions_1.SecuritySystemAreaCommand))
throw new Error('Invalid command ' + command);
// Load actual area banks to local table for ease of use
let actionableAreas = [];
let actualAreas = [];
for (let i of this.areas)
actualAreas.push(i.bank);
// Decipher input and prepare actionableAreas table for looping through
if (typeof (areas) == 'number')
actionableAreas.push(areas);
else if (Array.isArray(areas) && areas.length > 0)
actionableAreas = areas;
else
actionableAreas = actualAreas;
// For every area in actionableAreas:
for (let i of actionableAreas) {
// Check if the actual area exists
if (!actualAreas.includes(i))
throw new Error('Specified area ' + i + ' not found');
else {
// Prepare the payload according to details
const payload = {
'comm': 80,
'data0': 2,
'data1': (1 << i % 8),
'data2': command
};
this.log.debug('Sending area command: ' + command + " for area: " + i + "...");
// Finally make the request
await this.makeRequest('user/keyfunction.cgi', payload);
this.log.debug("Command send successfully.");
}
}
return true;
}
catch (error) {
throw (error);
}
}
async retrieveAreas(response = undefined) {
if (this.sessionID == "")
throw (new Error('Could not retrieve areas; not logged in'));
try {
this.log.debug("Retrieving areas...");
// If we are passed an already loaded Response use that, otherwise reload area.htm
if (response == undefined) {
response = await this.makeRequest('user/area.htm', { "sess": this.sessionID }, false);
}
// Get area sequence
let regexMatch = response.data.match(/var\s+areaSequence\s+=\s+new\s+Array\(([\d,]+)\);/);
let sequence = regexMatch[1].split(',');
// Get area states
regexMatch = response.data.match(/var\s+areaStatus\s+=\s+new\s+Array\(([\d,]+)\);/);
let bank_states = regexMatch[1].split(',');
// Get area names
regexMatch = response.data.match(/var\s+areaNames\s+=\s+new\s+Array\((\"(.+)\")\);/);
let area_names = regexMatch[1].split(',');
area_names.forEach((item, i, arr) => { arr[i] = decodeURI(item.replace(/['"]+/g, '')); });
// Pad sequence table to match the length of area_names table
if (area_names.length - sequence.length > 0) {
let filler = new Array(area_names.length - sequence.length);
filler.fill(0);
sequence = sequence.concat(filler);
}
// Reset class areas tables...
this.areas.length = 0;
// ... and populate it from scratch
area_names.forEach((name, i) => {
// If the name is "!" it's an empty area; ignore it
if (name == "%21" || name == "!")
return;
const startingState = Math.floor(i / 8) * 17;
// Create a new Area object and populate it with the area details, then push it
let newArea = {
bank: i,
name: (name == "" ? 'Area ' + (i + 1) : name),
priority: 6,
sequence: sequence[i],
bank_state: bank_states.slice(startingState, startingState + 17),
status: "",
states: {}
};
this.areas.push(newArea);
});
if (this.areas.length == 0)
throw new Error('No areas found; check your installation and/or user permissions');
this.log.debug("Retrieved " + this.areas.length + " areas successfully.");
this.processAreas();
}
catch (error) {
throw (error);
}
return;
}
async retrieveZones() {
if (this.sessionID == "")
throw (new Error('Could not retrieve zones; not logged in'));
try {
this.log.debug("Retrieving zones...");
// Retrieve zones.htm for parsing
const response = await this.makeRequest('user/zones.htm', { "sess": this.sessionID }, false);
// Get Zone sequences from response and store in class instance
let regexMatch = response.data.match(/var\s+zoneSequence\s+=\s+new\s+Array\(([\d,]+)\);/);
this.zonesSequence = regexMatch[1].split(',').filter((x) => x.trim().length && !isNaN(parseInt(x))).map(Number);
// Get Zone banks from response and store in class instance
this.zonesBank.length = 0;
regexMatch = response.data.match(/var\s+zoneStatus\s+=\s+new\s+Array\((.*)\);/);
regexMatch = regexMatch[1].matchAll(/(?:new\s+)?Array\((?<states>[^)]+)\)\s*(?=$|,\s*(?:new\s+)?Array\()/g);
for (const bank of regexMatch) {
this.zonesBank.push(bank[1].split(',').filter((x) => x.trim().length && !isNaN(parseInt(x))).map(Number));
}
// Retrieve zone names
regexMatch = response.data.match(/var zoneNames\s*=\s*(?:new\s+)?Array\(([^)]+)\).*/);
let zone_names = regexMatch[1].split(',');
zone_names.forEach((item, i, arr) => { arr[i] = decodeURI(item.replace(/['"]+/g, '')); });
// Firmware versions from 0.106 below don't allow for zone naming, so check for that
let zoneNaming = (parseFloat(this.version) > 0.106) ? true : false;
// Finally store the zones
// Reset class zones tables...
this.zones.length = 0;
// ... and populate it from scratch
this.zoneNameCount = zone_names.length;
this.zones = Array(this.zoneNameCount).fill(undefined);
zone_names.forEach((name, i) => {
// If the name is "!" it's an empty area; ignore it
if (name == "%21" || name == "!" || name == "%2D" || name == "-" || (zoneNaming && name == "")) {
i++;
return;
}
// Create a new Zone object and populate it with the zone details, then push it
let newZone = {
bank: i,
associatedArea: -1,
name: (zoneNaming ? (name == "" ? 'Sensor ' + (i + 1) : name) : ('Sensor ' + (i + 1))),
priority: 6,
sequence: 0,
bank_state: [],
status: "",
isBypassed: false,
autoBypass: false
};
this.zones[i] = newZone;
});
this.log.debug("Retrieved " + this.zones.length + " zones successfully.");
this.processZones();
}
catch (error) {
throw (error);
}
return;
}
async retrieveOutputs(response = undefined) {
if (this.sessionID == "")
throw (new Error('Could not retrieve outputs; not logged in'));
try {
this.log.debug("Retrieving outputs...");
// If we are passed an already loaded Response use that, otherwise reload outputs.htm
if (response == undefined) {
response = await this.makeRequest('user/outputs.htm', { "sess": this.sessionID }, false);
}
if (response == undefined)
throw new Error('Response came back undefined from the panel');
// Reset class outputs tables...
this.outputs.length = 0;
// Get output names
let regexMatch = response.data.matchAll(/var\s+oname\d+\s+=\s+decodeURIComponent\s*\(\s*decode_utf8\s*\(\s*\"(.*)\"\)\);/g);
let outputNames = [];
for (const name of regexMatch)
outputNames.push(name[1]);
// Get output values
regexMatch = response.data.matchAll(/var\s+ostate\d+\s+=\s+\"([0,1]{1})\";/g);
let outputValues = [];
for (const value of regexMatch)
outputValues.push(+(value[1]) == 1);
for (let i = 0; i < outputNames.length; i++) {
// Create a new Output object and populate it with the output details, then push it
let newOutput = {
bank: i,
name: (outputNames[i] == "" ? 'Output ' + (i + 1) : outputNames[i]),
status: outputValues[i]
};
this.outputs.push(newOutput);
}
}
catch (error) {
throw (error);
}
this.log.debug("Retrieved " + this.outputs.length + " outputs successfully.");
return;
}
processAreas() {
if (this.sessionID == "")
throw (new Error('Could not process areas; not logged in'));
// Loop through detected areas
this.areas.forEach(area => {
// Define mask for said area
let mask = (1 << (area.bank % 8));
// Create virtual states table for ease and readability
let vbank = [];
area.bank_state.forEach(state => {
vbank.push(state & mask);
});
// (Partially) Armed state, exit mode and chime setting booleans
let st_partial = Boolean(vbank[definitions_1.AreaBank.PARTIAL]);
let st_armed = Boolean(vbank[definitions_1.AreaBank.ARMED]);
let st_exit1 = Boolean(vbank[definitions_1.AreaBank.EXIT_MODE01]);
let st_exit2 = Boolean(vbank[definitions_1.AreaBank.EXIT_MODE02]);
let st_chime = Boolean(vbank[definitions_1.AreaBank.CHIME]);
// Priority starts from 6, which is the lowest; can go up to 1
let priority = 6;
let status = "";
// Start with index -1
let index = -1;
while (status == "") {
// Increment the index by 1
index++;
if (index >= definitions_1.AreaState.Priority.length) {
// If there are extra area status messages set go into this
if (this.__extra_area_status.length > 0) {
status = this.__extra_area_status[index - definitions_1.AreaState.Priority.length];
// Convert 'No System Faults' to 'Not Ready'
if (status == "No System Faults")
status = definitions_1.AreaState.Status[definitions_1.AreaState.State.NOT_READY_FORCEABLE];
else
status = definitions_1.AreaState.Status[definitions_1.AreaState.State.READY];
}
continue;
}
// Get virtual index based on priority
let v_index = definitions_1.AreaState.Priority[index];
if (vbank[v_index]) {
if (!(st_armed || st_partial) || definitions_1.AreaState.Status[v_index] !== definitions_1.AreaState.Status[definitions_1.AreaState.State.READY]) {
status = definitions_1.AreaState.Status[v_index];
}
if (definitions_1.AreaState.Status[v_index] !== definitions_1.AreaState.Status[definitions_1.AreaState.State.DELAY_EXIT_1]) {
// Bump to DELAY_EXIT_2, as it will eventually max out the while loop and move past that
index++;
}
}
else if (definitions_1.AreaState.Status[v_index] == definitions_1.AreaState.Status[definitions_1.AreaState.State.READY] && !(st_armed || st_partial)) {
status = definitions_1.AreaState.Status[definitions_1.AreaState.State.NOT_READY];
}
if (vbank[definitions_1.AreaBank.FIRE_ALARM] || vbank[definitions_1.AreaBank.BURGLAR_ALARM] || vbank[definitions_1.AreaBank.PANIC_ALARM] || vbank[definitions_1.AreaBank.MEDICAL_ALARM]) {
priority = 1;
}
else if (vbank[definitions_1.AreaBank.UNKWN_11] || vbank[definitions_1.AreaBank.UNKWN_12] || vbank[definitions_1.AreaBank.UNKWN_13] || vbank[definitions_1.AreaBank.UNKWN_14] || this.__extra_area_status.length > 0) {
priority = 2;
}
else if (vbank[definitions_1.AreaBank.UNKWN_10] || st_partial) {
priority = 3;
}
else if (st_armed) {
priority = 4;
}
else if (vbank[definitions_1.AreaBank.UNKWN_02]) {
priority = 5;
}
// Update the area with details
area.priority = priority;
area.status = status;
area.states = {
'armed': st_armed,
'partial': st_partial,
'chime': st_chime,
'exit1': st_exit1,
'exit2': st_exit2
};
}
});
return;
}
processZones() {
if (this.sessionID == "")
throw (new Error('Could not process zones; not logged in'));
this._zvbank = Array(this.zoneNameCount).fill([]);
// Loop through detected areas
this.zones.forEach(zone => {
if (zone == undefined)
return;
// Set our mask and initial offset
let mask = 1 << zone.bank % 16;
let index = Math.floor(zone.bank / 16);
// Set initial priority, starting with 5, which is the lowest
let priority = 5;
// Create a virtual zone state table for ease of reference
let vbank = [];
this.zonesBank.forEach(element => {
let value = Boolean(element[index] & mask);
vbank.push(value);
this._zvbank[zone.bank].push(value ? 1 : 0);
});
// Red zone status
if (vbank[definitions_1.ZoneState.State.UNKWN_05])
priority = 1;
// Blue zone status
if (vbank[definitions_1.ZoneState.State.UNKWN_01] || vbank[definitions_1.ZoneState.State.UNKWN_02] || vbank[definitions_1.ZoneState.State.UNKWN_06] || vbank[definitions_1.ZoneState.State.UNKWN_07])
priority = 2;
// Yellow zone status
if (vbank[definitions_1.ZoneState.State.BYPASSED] || vbank[definitions_1.ZoneState.State.UNKWN_04])
priority = 3;
// Grey zone status
if (vbank[definitions_1.ZoneState.State.UNKWN_00])
priority = 4;
let bank_no = 0;
let status = "";
while (status == "") {
if (vbank[bank_no])
status = definitions_1.ZoneState.Status[bank_no];
else if (bank_no == 0)
status = definitions_1.ZoneState.Ready;
bank_no++;
}
// Update our sequence
let sequence = zone.bank_state.join() !== this._zvbank[zone.bank].join() ? Utilities.nextSequence(zone.sequence) : zone.sequence;
// Update the zone with details
zone.priority = priority;
zone.status = status;
zone.bank_state = this._zvbank[zone.bank];
zone.sequence = sequence;
zone.isBypassed = vbank[definitions_1.ZoneState.State.BYPASSED];
zone.autoBypass = vbank[definitions_1.ZoneState.State.AUTOBYPASS];
zone.associatedArea = index;
});
return;
}
async poll() {
// Requesting a sequence response is a means of polling the panel for
// updates; if the sequence value changes, it means something has changed
// The index of the area or zone that changed indicates the entry that
// needs updating, but it is up to the user to call the corresponding
// update functions to perform the actual update
if (this.sessionID == "")
return false;
try {
const response = await this.makeRequest('user/seq.xml');
const json = new fast_xml_parser_1.XMLParser().parse(response.data)['response'];
const seqResponse = {
areas: typeof (json['areas']) == 'number' ? [json['areas']] : json['areas'].split(',').filter((x) => x.trim().length && !isNaN(parseInt(x))).map(Number),
zones: typeof (json['zones']) == 'number' ? [json['zones']] : json['zones'].split(',').filter((x) => x.trim().length && !isNaN(parseInt(x))).map(Number)
};
let performAreaUpdate = false;
let performZoneUpdate = false;
let index = 0;
// Check for zone updates first
for (index = 0; index < seqResponse.zones.length; index++) {
if (seqResponse.zones[index] != this.zonesSequence[index]) {
// Updating sequence and zone details now
this.zonesSequence[index] = seqResponse.zones[index];
await this.zoneStatusUpdate(index);
performZoneUpdate = true;
}
}
// Now check for area update
for (index = 0; index < seqResponse.areas.length; index++) {
if (seqResponse.areas[index] != this.areas[index].sequence) {
// Updating sequence and zone details now
this.areas[index].sequence = seqResponse.areas[index];
await this.areaStatusUpdate(index);
performAreaUpdate = true;
}
}
this.outputStatusUpdate();
// Trigger zone and area updates according to changes detected
if (performZoneUpdate)
this.processZones();
if (performAreaUpdate)
this.processAreas();
}
catch (error) {
throw (new Error("Error while polling: " + error.message));
}
return;
}
async zoneStatusUpdate(bank) {
if (this.sessionID == "")
throw (new Error('Could not fetch zone status; not logged in'));
// Fetch zone update
try {
const response = await this.makeRequest('user/zstate.xml', { 'state': bank });
const json = new fast_xml_parser_1.XMLParser().parse(response.data)['response'];
const zdat = typeof (json['zdat']) == 'number' ? [json['zdat']] : json['zdat'].split(',').filter((x) => x.trim().length && !isNaN(parseInt(x))).map(Number);
this.zonesBank[bank] = zdat;
}
catch (error) {
throw (error);
}
return;
}
async outputStatusUpdate() {
if (this.sessionID == "")
throw (new Error('Could not fetch output status; not logged in'));
// Fetch zone update
try {
const response = await this.makeRequest('user/outstat.xml');
const json = new fast_xml_parser_1.XMLParser().parse(response.data)['response'];
const values = Object.values(json);
for (let i = 0; i < this.outputs.length; i++) {
this.outputs[i].status = (values[i] == 0) ? false : true;
}
}
catch (error) {
throw (error);
}
return;
}
async areaStatusUpdate(bank) {
if (this.sessionID == "")
throw (new Error('Could not fetch area status; not logged in'));
// Fetch area update
try {
const response = await this.makeRequest('user/status.xml', { 'arsel': bank });
const json = new fast_xml_parser_1.XMLParser().parse(response.data)['response'];
if (json.hasOwnProperty('sysflt'))
this.__extra_area_status = json['sysflt'].split('\n');
else
this.__extra_area_status = [];
this.areas[bank].bank_state.length = 0;
for (let index = 0; index < 17; index++) {
this.areas[bank].bank_state.push(json["stat" + index]);
}
}
catch (error) {
throw (error);
}
return;
}
// This is a helper function that handles all network requests necessary,
// from logging in to polling for system changes and issuing area and zone
// commands. The payload contains all necessary parameters for the actual
// request, except for the session ID, which is supplied before the call
// itself takes place. attemptLogin is a special flag that is set to true
// only when calling makeRequest from the login and logout functions, and
// is used to check for invalid logins and to bypass the mutex lock for
// acquiring a new session on expiration
async makeRequest(address, payload = {}, shouldLock = true) {
let _shouldLock = shouldLock;
let release = (() => { });
try {
if (this.sessionID === "")
_shouldLock = false;
// Unless we are attempting to log in for the first time or if the session
// has expired, we should wait on our mutex to make sure that no calls are
// processed while the session ID is changing
if (_shouldLock)
release = await this.lock.acquire();
// Finish the payload by including the current session ID
// Fun fact: the NX-595E implementation of HTTP server *demands* that the
// session ID comes first in the payload, otherwise the call returns 302,
// therefore we have to create a local final payload with the 'sess' value
// first in the object before passing it to the axios client
const _payload = { ...(_shouldLock) ? { 'sess': this.sessionID } : {}, ...payload };
// Make the actual call with the complete payload
const response = await this.client({
url: address,
data: _payload
});
// If the response status code is in the 30x range, we have been sent
// back to the login page, which - in turn - means that either the session
// has expired, or we actually attempted to log in and our login info was
// invalid; in all other cases, return the response to the caller
if (response.status < 300)
return response;
else {
// If we are attempting to log in, the credentials provided were invalid
// and we have to abort...
if (!_shouldLock)
throw new Error("Login unsuccessful.");
// ... otherwise our session has expired and we need to log in again to
// refresh it; bear in mind that the three retrieving functions of the
// plugin (retrieveAreas, retrieveZones and retrieveOutputs) are called
// only from the login function, so they are called by definition as not
// locking, as they would deadlock the program in the first attempt to
// refresh the session
this.log.debug("Session expired; attempting to reacquire...");
await this.login();
this.log.debug("Reacquired session successfully.");
// We'll create a new local payload with the new session ID and retry
this.log.debug("Reattempting request...");
const _newPayload = { ...{ 'sess': this.sessionID }, ...payload };
const newResponse = await this.client({
url: address,
data: _newPayload
});
this.log.debug("Request sent successfully.");
if (newResponse.status >= 300)
throw new Error("Request denied by server with code " + newResponse.status);
return newResponse;
}
}
catch (error) {
throw error;
}
finally {
// Call release before exiting the function; if a lock was requested, it
// will unlock the mutex, otherwise it will call an empty arrow function
// that does nothing ( () => {} ).
release();
}
}
async sendOutputCommand(command, output) {
try {
if (this.sessionID === "") {
await this.login();
if (this.sessionID === "")
throw (new Error('Could not send output command; not logged in'));
}
if ((output >= this.outputs.length) || (output < 0))
throw new Error('Specified output ' + output + ' is out of bounds');
// Prepare the payload according to details
const payload = {
'onum': output + 1,
'ostate': (command) ? 1 : 0
};
// Finally make the request
this.log.debug('Sending output command: ' + command + " for output: " + output + "...");
await this.makeRequest('user/output.cgi', payload);
this.log.debug("Command sent successfully.");
return true;
}
catch (error) {
throw (error);
}
}
async sendZoneCommand(command = definitions_1.SecuritySystemZoneCommand.ZONE_BYPASS, zones = []) {
try {
if (this.sessionID === "") {
await this.login();
if (this.sessionID === "")
throw (new Error('Could not send zone command; not logged in'));
}
if (!(command in definitions_1.SecuritySystemZoneCommand))
throw new Error('Invalid zone state ' + command);
// Load actual area banks to local table for ease of use
let actionableZones = [];
let actualZones = [];
for (let i of this.zones) {
if (i == undefined)
continue;
actualZones.push(i.bank);
}
// Decipher input and prepare actionableAreas table for looping through
if (typeof (zones) == 'number')
actionableZones.push(zones);
else if (Array.isArray(zones) && zones.length > 0)
actionableZones = zones;
else
actionableZones = actualZones;
// For every area in actionableAreas:
for (let i of actionableZones) {
// Check if the actual area exists
if (!actualZones.includes(i))
throw new Error('Specified area ' + i + ' not found');
else {
// Prepare the payload according to details
const payload = {
'comm': 82,
'data0': i
};
// At present the only zone command is zone bypass so no need to pass on an actual command
// payload['data1'] = String(command);
// Finally make the request
this.log.debug('Sending zone command: ' + command + " for zone: " + i + "...");
await this.makeRequest('user/zonefunction.cgi', payload);
this.log.debug("Command sent successfully.");
}
}
return true;
}
catch (error) {
throw (error);
}
}
// All the following are accessor functions for system details
getZones() {
return this.zones;
}
getOutputs() {
return this.outputs;
}
getAreas() {
return this.areas;
}
getZoneState(zone) {
return !(this.zones[zone].status == definitions_1.ZoneState.Ready);
}
getOutputState(output) {
return (this.outputs[output].status == true);
}
getZoneBankState(zone) {
return (this.zones[zone].bank_state.join(''));
}
getAreaStatus(area) {
return (this.areas[area].status);
}
getAreaChimeStatus(area) {
return (this.areas[area].states["chime"]);
}
getAreaArmStatus(area) {
return (this.areas[area].states["armed"] || this.areas[area].states["partial"] || this.areas[area].states["exit1"] || this.areas[area].states["exit2"]);
}
getFirmwareVersion() {
return ('v' + this.version + '-' + this.release);
}
}
exports.NX595ESecuritySystem = NX595ESecuritySystem;
//# sourceMappingURL=NX595ESecuritySystem.js.map