homebridge-caddx-interlogix
Version:
A homebridge plugin for integrating the ComNav/NetworX/CaddX NX-595E network module with HomeKit
492 lines • 28.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NX595EPlatform = void 0;
const NX595ESecuritySystem_1 = require("./NX595ESecuritySystem");
const definitions_1 = require("./definitions");
const settings_1 = require("./settings");
const platformAccessory_1 = require("./platformAccessory");
/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
class NX595EPlatform {
constructor(log, config, api) {
this.log = log;
this.config = config;
this.api = api;
this.Service = this.api.hap.Service;
this.Characteristic = this.api.hap.Characteristic;
// this is used to track restored cached accessories
this.accessories = [];
this.areaSequences = [];
this.zoneSequences = [];
this.zoneDeltas = [];
this.radarPersistence = 60000;
this.smokePersistence = 60000;
this.displayBypassSwitches = false;
this.displayOutputSwitches = false;
this.useHTTPS = false;
this.loggedIn = false;
const username = this.config.username;
const pin = this.config.pin;
const ip = this.config.ip;
this.pollTimer = this.config.pollTimer;
this.displayBypassSwitches = (this.config.displayBypassSwitches) ? this.config.displayBypassSwitches : false;
this.displayOutputSwitches = (this.config.displayOutputSwitches) ? this.config.displayOutputSwitches : false;
this.radarPersistence = (this.config.radarPersistence) ? this.config.radarPersistence : 60000;
this.smokePersistence = (this.config.smokePersistence) ? this.config.smokePersistence : 60000;
this.securitySystem = new NX595ESecuritySystem_1.NX595ESecuritySystem(ip, username, pin, log, this.useHTTPS);
this.log.debug('Initialized security system.');
this.api.on('didFinishLaunching', async () => {
this.log.debug('Executed didFinishLaunching callback');
try {
// Attempt to log in
await this.securitySystem.login();
this.loggedIn = true;
this.log.debug('Logged in successfully.');
this.log.debug('Discovering devices...');
// run the method to discover / register your devices as accessories
this.discoverDevices();
this.log.debug('Devices discovery complete.');
this.areaSequences = new Array(this.securitySystem.getAreas().length);
this.zoneSequences = new Array(this.securitySystem.getZones().length);
this.zoneDeltas = new Array(this.securitySystem.getZones().length);
this.areaSequences.fill(-1);
this.zoneSequences.fill(-1);
this.zoneDeltas.fill(-1);
setTimeout(this.updateAccessories.bind(this), this.pollTimer);
}
catch (error) {
this.loggedIn = false;
this.log.error(error.message);
this.setCatastrophe(this.accessories);
}
});
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory) {
this.log.info('Loading accessory from cache:', accessory.displayName);
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory);
}
didCompleteLogin() {
return this.loggedIn;
}
setCatastrophe(accessories) {
accessories.forEach((accessory) => {
accessory.services
.filter((service) => service.UUID != this.Service.AccessoryInformation)
.forEach((service) => {
service.characteristics.forEach((characteristic) => {
characteristic.updateValue(new Error("Platform failed to initialize"));
// characteristic.on('get', (next) => {
// next(new Error("Platform failed to initialize"))
// })
});
});
});
}
async updateAccessories() {
let accService = undefined;
if (this.securitySystem == undefined || this.didCompleteLogin() == false) {
this.log.error('Platform not properly initialized; server unavailable or login unsuccessful');
return;
}
this.securitySystem.poll()
.then(() => {
this.securitySystem.getZones().forEach(zone => {
if (zone == undefined)
return;
const accessoriesUpdated = this.accessories.filter(accessory => accessory.context.device.bank === zone.bank);
const zoneStatus = this.securitySystem.getZoneState(zone.bank);
if (this.zoneSequences[zone.bank] !== zone.sequence) {
this.zoneSequences[zone.bank] = zone.sequence;
if (accessoriesUpdated.length) {
accessoriesUpdated.forEach(accessory => {
if (accessory.context.device.type != definitions_1.DeviceType.area) {
if (this.displayBypassSwitches) {
accService = accessory.getService(this.Service.Switch);
if (accService)
accService.getCharacteristic(this.Characteristic.On).updateValue(zone.isBypassed);
}
switch (accessory.context.device.type) {
case definitions_1.DeviceType.radar: {
if (zoneStatus) {
this.log.debug('Persistence updated for zone', zone.bank);
this.zoneDeltas[zone.bank] = Date.now();
}
break;
}
case definitions_1.DeviceType.smoke: {
if (zoneStatus) {
this.log.debug('Persistence updated for zone', zone.bank);
this.zoneDeltas[zone.bank] = Date.now();
}
break;
}
default:
case definitions_1.DeviceType.contact: {
accService = accessory.getService(this.Service.ContactSensor);
if (accService)
accService.getCharacteristic(this.Characteristic.ContactSensorState).updateValue(zoneStatus);
break;
}
}
}
});
}
}
if (accessoriesUpdated.length) {
accessoriesUpdated.forEach(accessory => {
if (accessory.context.device.type != definitions_1.DeviceType.area) {
const currentDelta = Date.now() - this.zoneDeltas[zone.bank];
switch (accessory.context.device.type) {
case definitions_1.DeviceType.radar: {
accService = accessory.getService(this.Service.MotionSensor);
if (this.zoneDeltas[zone.bank] >= 0 && currentDelta < this.radarPersistence) {
if (accService)
accService.getCharacteristic(this.Characteristic.MotionDetected).updateValue(true);
}
else {
this.zoneDeltas[zone.bank] = -1;
if (accService)
accService.getCharacteristic(this.Characteristic.MotionDetected).updateValue(false);
}
break;
}
case definitions_1.DeviceType.smoke: {
accService = accessory.getService(this.Service.SmokeSensor);
if (this.zoneDeltas[zone.bank] >= 0 && currentDelta < this.smokePersistence) {
if (accService)
accService.getCharacteristic(this.Characteristic.SmokeDetected).updateValue(true);
}
else {
this.zoneDeltas[zone.bank] = -1;
if (accService)
accService.getCharacteristic(this.Characteristic.SmokeDetected).updateValue(false);
}
break;
}
default: {
break;
}
}
}
});
}
});
this.securitySystem.getAreas().forEach(area => {
if (this.areaSequences[area.bank] !== area.sequence) {
this.areaSequences[area.bank] = area.sequence;
const accessoriesUpdated = this.accessories.filter(accessory => accessory.context.device.bank === area.bank);
if (accessoriesUpdated.length) {
accessoriesUpdated.forEach(accessory => {
if (accessory.context.device.type === definitions_1.DeviceType.area) {
const status = this.securitySystem.getAreaStatus(area.bank);
const chimeState = this.securitySystem.getAreaChimeStatus(area.bank);
let value = this.Characteristic.SecuritySystemCurrentState.DISARMED;
switch (status) {
case definitions_1.AreaState.Status[definitions_1.AreaState.State.ALARM_FIRE]:
case definitions_1.AreaState.Status[definitions_1.AreaState.State.ALARM_BURGLAR]:
case definitions_1.AreaState.Status[definitions_1.AreaState.State.ALARM_PANIC]:
case definitions_1.AreaState.Status[definitions_1.AreaState.State.ALARM_MEDICAL]: {
value = this.Characteristic.SecuritySystemCurrentState.ALARM_TRIGGERED;
break;
}
case definitions_1.AreaState.Status[definitions_1.AreaState.State.DELAY_EXIT_1]:
case definitions_1.AreaState.Status[definitions_1.AreaState.State.DELAY_EXIT_2]:
case definitions_1.AreaState.Status[definitions_1.AreaState.State.DISARMED]:
case definitions_1.AreaState.Status[definitions_1.AreaState.State.NOT_READY]:
case definitions_1.AreaState.Status[definitions_1.AreaState.State.SENSOR_BYPASS]:
case definitions_1.AreaState.Status[definitions_1.AreaState.State.READY]: {
value = this.Characteristic.SecuritySystemCurrentState.DISARMED;
break;
}
case definitions_1.AreaState.Status[definitions_1.AreaState.State.ARMED_STAY]: {
value = this.Characteristic.SecuritySystemCurrentState.STAY_ARM;
break;
}
case definitions_1.AreaState.Status[definitions_1.AreaState.State.DELAY_ENTRY]:
case definitions_1.AreaState.Status[definitions_1.AreaState.State.ARMED_AWAY]: {
value = this.Characteristic.SecuritySystemCurrentState.AWAY_ARM;
break;
}
default: {
break;
}
}
accService = accessory.getService(this.Service.Switch);
if (accService)
accService.getCharacteristic(this.Characteristic.On).updateValue(chimeState);
accService = accessory.getService(this.Service.SecuritySystem);
if (accService) {
if (status === definitions_1.AreaState.Status[definitions_1.AreaState.State.DELAY_EXIT_2] ||
status === definitions_1.AreaState.Status[definitions_1.AreaState.State.DELAY_EXIT_1])
accService.getCharacteristic(this.Characteristic.SecuritySystemTargetState).updateValue(this.Characteristic.SecuritySystemCurrentState.AWAY_ARM);
else
accService.getCharacteristic(this.Characteristic.SecuritySystemTargetState).updateValue(value);
accService.getCharacteristic(this.Characteristic.SecuritySystemCurrentState).updateValue(value);
}
}
});
}
}
});
}).catch((error) => {
this.log.error(error.message);
}).finally(() => {
setTimeout(this.updateAccessories.bind(this), this.pollTimer);
});
}
/**
* This is an example method showing how to register discovered accessories.
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
*/
discoverDevices() {
let devices;
devices = [];
if (this.securitySystem == undefined)
return;
if (this.didCompleteLogin() == false)
return;
this.securitySystem.getOutputs().forEach(output => {
this.log.debug('Detected output: ', output.name);
devices.push({
type: definitions_1.DeviceType.output,
uniqueID: output.bank + '#' + output.name,
bank: output.bank,
bank_state: output.status,
displayName: output.name,
firmwareVersion: this.securitySystem.getFirmwareVersion(),
shouldIgnore: (this.displayOutputSwitches) ? false : true
});
});
this.securitySystem.getAreas().forEach(area => {
this.log.debug('Detected area: ', area.name);
devices.push({
type: definitions_1.DeviceType.area,
uniqueID: area.bank + '#' + area.name,
bank: area.bank,
bank_state: area.bank_state,
displayName: area.name,
firmwareVersion: this.securitySystem.getFirmwareVersion(),
});
});
// Populate ignore zones table from configuration
// Zone ignoring should be declared in plugin config as a string of numbers
// separated by commas; ranges are allowed as well
// Negative zone indexes, invalid ranges and out-of-index zones cause an
// exception to be thrown
// Examples:
// 1,2,8
// 3-5
// 1,4-6,8,11,23-28
let ignores = new Array(this.securitySystem.getZones().length).fill(false);
const ignoreString = (this.config.ignoreZones) ? this.config.ignoreZones : undefined;
if (ignoreString != undefined) {
if ((new RegExp("^\\d{1,3}(?:-\\d{1,3})?(?:,\\d{1,3}(?:-\\d{1,3})?)*$")).test(ignoreString)) {
const ignoreZones = ignoreString.split(',');
ignoreZones.forEach(element => {
if (new RegExp('^[0-9]+$').test(element)) {
const ignoreIndex = parseInt(element);
if (ignoreIndex > ignores.length) {
throw new Error("Zone " + element + " required to ignore exceeds zone count!");
}
else {
ignores[ignoreIndex - 1] = true;
this.log.debug('Zone ', ignoreIndex, ' ignored');
}
}
else {
const ignoreRange = element.split('-');
const rangeStart = parseInt(ignoreRange[0]);
const rangeEnd = parseInt(ignoreRange[1]);
if (rangeStart > ignores.length || rangeEnd > ignores.length) {
throw new Error("Zone range " + element + " required to ignore violates zone count!");
}
else {
this.log.debug('Ignoring zone range: ', rangeStart, '-', rangeEnd);
const rangeDiff = rangeEnd - rangeStart;
if (rangeDiff <= 0) {
throw new Error("Zone ranges should be declared from lower to higer zone index (zone range was: " + element + ")!");
}
else {
for (let i = rangeStart; i <= rangeEnd; i++) {
ignores[i - 1] = true;
this.log.debug('Zone ', i, ' ignored');
}
}
}
}
});
}
else {
throw new Error('Ignore zones string "' + ignoreString + '" has wrong syntax!');
}
}
// Get override zones table from configuration
// Overrides have a zone index specified; in case the user has declared
// multiple overrides for the same zone index, only the last one applies
const declaredOverrides = (this.config.override) ? this.config.override : [];
let overrides = new Array(ignores.length).fill(undefined);
declaredOverrides.forEach((element) => {
if (element.index < 1 || element.index > overrides.length) {
throw new Error("Override declared for non-existent zone with index " + element.index + "!");
}
overrides[element.index - 1] = element;
});
this.securitySystem.getZones().forEach(zone => {
if (zone == undefined)
return;
const shouldOverride = (overrides[zone.bank] != undefined) ? true : false;
const zoneName = (shouldOverride && overrides[zone.bank].name && overrides[zone.bank].name !== "") ? overrides[zone.bank].name : zone.name;
this.log.debug('Detected zone: ', zone.name);
let deviceType = definitions_1.DeviceType.contact;
if (shouldOverride) {
switch (overrides[zone.bank].sensor) {
case "Radar": {
deviceType = definitions_1.DeviceType.radar;
break;
}
case "Smoke": {
deviceType = definitions_1.DeviceType.smoke;
break;
}
case "Contact":
default: {
break;
}
}
}
devices.push({
type: deviceType,
uniqueID: zone.bank + '#' + zone.name,
bank: zone.bank,
associatedArea: zone.associatedArea,
bank_state: this.securitySystem.getZoneBankState(zone.bank),
displayName: zoneName,
firmwareVersion: this.securitySystem.getFirmwareVersion(),
shouldIgnore: ignores[zone.bank]
});
});
// loop over the discovered devices and register each one if it has not already been registered
let device;
for (device of devices) {
// generate a unique id for the accessory this should be generated from
// something globally unique, but constant, for example, the device serial
// number or MAC address
const uuid = this.api.hap.uuid.generate(device.uniqueID);
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory && device.type == existingAccessory.context.device.type) {
// the accessory already exists
if (device) {
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
// existingAccessory.context.device = device;
// this.api.updatePlatformAccessories([existingAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
if (device.type != definitions_1.DeviceType.area && device.shouldIgnore) {
this.log.info('Removing existing accessory from cache:', existingAccessory.displayName);
this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [existingAccessory]);
}
else {
this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName);
existingAccessory.context.device = device;
this.log.debug(device.bank_state);
switch (device.type) {
case definitions_1.DeviceType.area: {
new platformAccessory_1.NX595EPlatformSecurityAreaAccessory(this, existingAccessory, this.securitySystem);
break;
}
case definitions_1.DeviceType.output: {
if (device.shouldIgnore == false) {
new platformAccessory_1.NX595EPlatformOutputAccessory(this, existingAccessory);
}
break;
}
case definitions_1.DeviceType.radar: {
if (device.shouldIgnore == false) {
new platformAccessory_1.NX595EPlatformRadarAccessory(this, existingAccessory, this.displayBypassSwitches);
}
break;
}
case definitions_1.DeviceType.smoke: {
if (device.shouldIgnore == false) {
new platformAccessory_1.NX595EPlatformSmokeSensorAccessory(this, existingAccessory, this.displayBypassSwitches);
}
break;
}
case definitions_1.DeviceType.contact:
default: {
if (device.shouldIgnore == false) {
new platformAccessory_1.NX595EPlatformContactSensorAccessory(this, existingAccessory, this.displayBypassSwitches);
}
break;
}
}
// update accessory cache with any changes to the accessory details and information
this.api.updatePlatformAccessories([existingAccessory]);
}
}
else if (!device) {
// it is possible to remove platform accessories at any time using `api.unregisterPlatformAccessories`, eg.:
// remove platform accessories when no longer present
this.log.info('Removing existing accessory from cache:', existingAccessory.displayName);
this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [existingAccessory]);
}
}
else {
if (existingAccessory) {
this.log.info('Removing existing accessory from cache:', existingAccessory.displayName);
this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [existingAccessory]);
}
// the accessory does not yet exist, so we need to create it
// create a new accessory
const accessory = new this.api.platformAccessory(device.displayName, uuid);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.context.device = device;
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
if (device.type == definitions_1.DeviceType.area || device.shouldIgnore == false) {
switch (device.type) {
case definitions_1.DeviceType.area: {
new platformAccessory_1.NX595EPlatformSecurityAreaAccessory(this, accessory, this.securitySystem);
break;
}
case definitions_1.DeviceType.output: {
new platformAccessory_1.NX595EPlatformOutputAccessory(this, accessory);
break;
}
case definitions_1.DeviceType.radar: {
new platformAccessory_1.NX595EPlatformRadarAccessory(this, accessory, this.displayBypassSwitches);
break;
}
case definitions_1.DeviceType.smoke: {
new platformAccessory_1.NX595EPlatformSmokeSensorAccessory(this, accessory, this.displayBypassSwitches);
break;
}
case definitions_1.DeviceType.contact:
default: {
new platformAccessory_1.NX595EPlatformContactSensorAccessory(this, accessory, this.displayBypassSwitches);
break;
}
}
// link the accessory to your platform
this.log.info('Adding new accessory:', device.displayName);
this.log.debug(device.bank_state);
this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
}
}
}
}
}
exports.NX595EPlatform = NX595EPlatform;
//# sourceMappingURL=platform.js.map