Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add manual discovery setting #3

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -606,4 +634,4 @@
}
}
}
}
}
154 changes: 94 additions & 60 deletions src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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<iend; ++i) {
const manualDiscovery = this.manualDiscovery[i];
const deviceLocation: string = 'http://' + manualDiscovery.ipAddress + ':' + manualDiscovery.port.toString() + '/Device.xml';
this.log.info('Probing ' + deviceLocation);
fetch(deviceLocation)
.then(response => response.text())
.then(data => {
const panelUUID: string = data.match(/<UDN>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));
Expand Down Expand Up @@ -1289,4 +1323,4 @@ export class KonnectedHomebridgePlatform implements DynamicPlatformPlugin {
*/
}
}
}
}