diff --git a/config.schema.json b/config.schema.json index 72cc7b7..a0a51b8 100755 --- a/config.schema.json +++ b/config.schema.json @@ -41,6 +41,34 @@ "maximum": 30, "placeholder": "(default: 5)" }, + "manualDiscovery": { + "title": "Manual IP (optional)", + "description": "Disables auto discovery mode and manually configures IP address and port of Konnected Alarm Panels on the network.", + "buttonText": "Add IP Address", + "type": "array", + "orderable": true, + "expandable": true, + "expanded": false, + "items": { + "type": "object", + "properties": { + "ipAddress": { + "title": "IP Address (must be static)", + "type": "string", + "format": "ipv4", + "required": true + }, + "port": { + "title": "Port", + "type": "number", + "step": 1, + "minimum": 8000, + "maximum": 24777, + "required": true + } + } + } + }, "entryDelaySettings": { "type": "object", "expandable": true, @@ -606,4 +634,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/platform.ts b/src/platform.ts index 7d29ab2..8cf30ce 100755 --- a/src/platform.ts +++ b/src/platform.ts @@ -85,7 +85,7 @@ export class KonnectedHomebridgePlatform implements DynamicPlatformPlugin { private listenerAuth: string[] = []; // for storing random auth strings private ssdpDiscovering = false; // for storing state of SSDP discovery process private ssdpDiscoverAttempts = 0; - + private manualDiscovery = this.config.advanced?.manualDiscovery ? this.config.advanced.manualDiscovery : []; constructor(public readonly log: Logger, public readonly config: PlatformConfig, public readonly api: API) { this.log.debug('Finished initializing platform'); @@ -315,75 +315,109 @@ export class KonnectedHomebridgePlatform implements DynamicPlatformPlugin { * @reference Alarm Panel V1-V2: urn:schemas-konnected-io:device:Security:1 * @reference Alarm Panel Pro: urn:schemas-konnected-io:device:Security:2 */ + discoveredPanel(panelUUID, ssdpHeaderLocation, ssdpDeviceIDs, excludedUUIDs) { + // dedupe responses, ignore excluded panels in environment variables, and then provision panel(s) + if (!ssdpDeviceIDs.includes(panelUUID) && !excludedUUIDs.includes(panelUUID)) { + // get panel status object (not using async await) + fetch(ssdpHeaderLocation.replace('Device.xml', 'status')) + // convert response to JSON + .then((fetchResponse) => fetchResponse.json()) + .then((panelResponseObject) => { + // create listener object to pass back to panel when provisioning it + const listenerObject = { + ip: this.listenerIP, + port: this.listenerPort, + }; + + // use the above information to construct panel in Homebridge config + this.updateHomebridgeConfig(panelUUID, panelResponseObject); + + // if the settings property does not exist in the response, + // then we have an unprovisioned panel + if (Object.keys(panelResponseObject.settings).length === 0) { + this.provisionPanel(panelUUID, panelResponseObject, listenerObject); + } else { + if (panelResponseObject.settings.endpoint_type === 'rest') { + const panelBroadcastEndpoint = new URL(panelResponseObject.settings.endpoint); + + // if the IP address or port are not the same, reprovision endpoint component + if ( + panelBroadcastEndpoint.host !== this.listenerIP || + Number(panelBroadcastEndpoint.port) !== this.listenerPort + ) { + this.provisionPanel(panelUUID, panelResponseObject, listenerObject); + } + } else if (panelResponseObject.settings.endpoint_type === 'aws_iot') { + this.log.error( + `ERROR: Cannot provision panel ${panelUUID} with Homebridge. Panel has previously been provisioned with another platform (Konnected Cloud, SmartThings, Home Assistant, Hubitat,. etc). Please factory reset your Konnected Alarm panel and disable any other platform connectors before associating the panel with Homebridge.` + ); + } + } + }); + + // add the UUID to the deduping array + ssdpDeviceIDs.push(panelUUID); + } + } + discoverPanels() { - const ssdpClient = new client.Client(); - const ssdpUrnPartial = 'urn:schemas-konnected-io:device'; const ssdpDeviceIDs: string[] = []; // used later for deduping SSDP reflections const excludedUUIDs: string[] = String(process.env.KONNECTED_EXCLUDES).split(','); // used for ignoring specific panels (mostly for development) // set discovery state this.ssdpDiscovering = true; - // begin discovery - ssdpClient.search('ssdp:all'); - - // on discovery - ssdpClient.on('response', (headers) => { - // check for only Konnected devices - if (headers.ST!.indexOf(ssdpUrnPartial) !== -1) { - // store reported URL of panel that responded - const ssdpHeaderLocation: string = headers.LOCATION || ''; - // extract UUID of panel from the USN string - const panelUUID: string = headers.USN!.match(/^uuid:(.*)::.*$/i)![1] || ''; - - // dedupe responses, ignore excluded panels in environment variables, and then provision panel(s) - if (!ssdpDeviceIDs.includes(panelUUID) && !excludedUUIDs.includes(panelUUID)) { - // get panel status object (not using async await) - fetch(ssdpHeaderLocation.replace('Device.xml', 'status')) - // convert response to JSON - .then((fetchResponse) => fetchResponse.json()) - .then((panelResponseObject) => { - // create listener object to pass back to panel when provisioning it - const listenerObject = { - ip: this.listenerIP, - port: this.listenerPort, - }; - - // use the above information to construct panel in Homebridge config - this.updateHomebridgeConfig(panelUUID, panelResponseObject); - - // if the settings property does not exist in the response, - // then we have an unprovisioned panel - if (Object.keys(panelResponseObject.settings).length === 0) { - this.provisionPanel(panelUUID, panelResponseObject, listenerObject); - } else { - if (panelResponseObject.settings.endpoint_type === 'rest') { - const panelBroadcastEndpoint = new URL(panelResponseObject.settings.endpoint); - - // if the IP address or port are not the same, reprovision endpoint component - if ( - panelBroadcastEndpoint.host !== this.listenerIP || - Number(panelBroadcastEndpoint.port) !== this.listenerPort - ) { - this.provisionPanel(panelUUID, panelResponseObject, listenerObject); - } - } else if (panelResponseObject.settings.endpoint_type === 'aws_iot') { - this.log.error( - `ERROR: Cannot provision panel ${panelUUID} with Homebridge. Panel has previously been provisioned with another platform (Konnected Cloud, SmartThings, Home Assistant, Hubitat,. etc). Please factory reset your Konnected Alarm panel and disable any other platform connectors before associating the panel with Homebridge.` - ); - } - } - }); - - // add the UUID to the deduping array - ssdpDeviceIDs.push(panelUUID); - } + let ssdpClient; + + if (this.manualDiscovery.length) { + // manual discovery probe ip:port for device info + this.log.debug('Manual discovery enabled attempting connect to modules'); + for (let i=0, iend=this.manualDiscovery.length; i response.text()) + .then(data => { + const panelUUID: string = data.match(/uuid:(.*?)<\/UDN>/i)![1] || ''; + if (panelUUID !== '') { + this.log.info(`Manual discovery found panel ${deviceLocation} -- ${panelUUID}`); + this.discoveredPanel(panelUUID, deviceLocation, ssdpDeviceIDs, excludedUUIDs); + } else { + this.log.info(`Manual discovery invalid response from ${deviceLocation} -- ${data}`); + } + }) + .catch (error => { + this.log.error('Manual discovery failed for ' + manualDiscovery.ipAddress + ':' + manualDiscovery.port + ' - ' + error); + }); } - }); + } else { + this.log.debug('Automatic discovery enabled starting ssdpClient'); + + ssdpClient = new client.Client(); + const ssdpUrnPartial = 'urn:schemas-konnected-io:device'; + + // begin discovery + ssdpClient.search('ssdp:all'); + + // on discovery + ssdpClient.on('response', (headers) => { + // check for only Konnected devices + if (headers.ST!.indexOf(ssdpUrnPartial) !== -1) { + // store reported URL of panel that responded + const ssdpHeaderLocation: string = headers.LOCATION || ''; + // extract UUID of panel from the USN string + const panelUUID: string = headers.USN!.match(/^uuid:(.*)::.*$/i)![1] || ''; + this.discoveredPanel(panelUUID, ssdpHeaderLocation, ssdpDeviceIDs, excludedUUIDs); + } + }); + } // stop discovery after a number of seconds seconds, default is 5 setTimeout(() => { - ssdpClient.stop(); + if (ssdpClient !== undefined) { + ssdpClient.stop(); + } this.ssdpDiscovering = false; if (ssdpDeviceIDs.length) { this.log.debug('Discovery complete. Found panels:\n' + JSON.stringify(ssdpDeviceIDs, null, 2)); @@ -1289,4 +1323,4 @@ export class KonnectedHomebridgePlatform implements DynamicPlatformPlugin { */ } } -} \ No newline at end of file +}