UNPKG

homebridge-caddx-interlogix

Version:

A homebridge plugin for integrating the ComNav/NetworX/CaddX NX-595E network module with HomeKit

492 lines 28.5 kB
"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