diff --git a/src/3rdParty/HapClient/eventedHttpClient/index.ts b/src/3rdParty/HapClient/eventedHttpClient/index.ts index 008500d..75c2405 100755 --- a/src/3rdParty/HapClient/eventedHttpClient/index.ts +++ b/src/3rdParty/HapClient/eventedHttpClient/index.ts @@ -8,10 +8,24 @@ import * as url from 'url'; import httpMessageParser from './/httpParser'; import { Dictionary } from '../../../Types/types'; +interface IRequest { + method: 'PUT' | 'POST' | 'GET' | 'DELETE' | 'PATCH'; + url: string; + maxAttempts: number; + headers: Dictionary; + body: string; +} + export const parseMessage = httpMessageParser; -export function createConnection(instance: { ipAddress: string, port: number }, pin: string, body: any) { - const client = net.createConnection({ +/** + * Create a socket connection. + * @param instance The host connection params + * @param pin The authorization token + * @param body The connection body + */ +export function createConnection(instance: { ipAddress: string, port: number }, pin: string, body: any): net.Socket { + const client: net.Socket = net.createConnection({ host: instance.ipAddress, port: instance.port, }); @@ -40,7 +54,7 @@ function _headersToString(headers: Dictionary) { return (response); } -function _buildMessage(request: any) { +function _buildMessage(request: IRequest) { const context = url.parse(request.url); let message; diff --git a/src/3rdParty/HapClient/index.ts b/src/3rdParty/HapClient/hapClient.ts similarity index 65% rename from src/3rdParty/HapClient/index.ts rename to src/3rdParty/HapClient/hapClient.ts index d22b107..d017782 100755 --- a/src/3rdParty/HapClient/index.ts +++ b/src/3rdParty/HapClient/hapClient.ts @@ -8,7 +8,7 @@ import { get, put } from 'request-promise-native'; import { Services, Characteristics } from './hap-types'; import { HapMonitor } from './monitor'; -import { IHapAccessoriesRespType, IServiceType, ICharacteristicType, IHapInstance, createDefaultCharacteristicType } from './interfaces'; +import { IHapAccessoriesRespType, IServiceType, ICharacteristicType, IHapInstance, createDefaultCharacteristicType, IAccessoryResp } from './interfaces'; export type HapAccessoriesRespType = IHapAccessoriesRespType; export type ServiceType = IServiceType; @@ -33,7 +33,12 @@ export interface IDevice { type: string } -export class HapClient extends EventEmitter { +interface IConfig { + debug?: boolean; + instanceBlacklist?: string[]; +} + +export class HapClient { private bonjour = Bonjour(); private browser?: Bonjour.Browser; private discoveryInProgress = false; @@ -41,10 +46,10 @@ export class HapClient extends EventEmitter { private log: (msg: string) => void; private pin: string; private debugEnabled: boolean; - private config: { - debug?: boolean; - instanceBlacklist?: string[]; - }; + private config: IConfig = { + debug: true, + instanceBlacklist: [], + } private instances: IHapInstance[] = []; @@ -61,13 +66,10 @@ export class HapClient extends EventEmitter { logger?: any; config: any; }) { - super(); - this.pin = opts.pin; this.log = opts.logger; this.debugEnabled = true; this.config = opts.config; - this.startDiscovery(); } private debug(msg: any) { @@ -84,7 +86,7 @@ export class HapClient extends EventEmitter { public refreshInstances() { if (!this.discoveryInProgress) { - this.startDiscovery(); + this.discover(); } else { try { this.debug(`[HapClient] Discovery :: Re-broadcasting discovery query`); @@ -92,118 +94,123 @@ export class HapClient extends EventEmitter { this.browser.update(); } - } catch (e) { } + } catch{ } } } - private async startDiscovery() { - this.discoveryInProgress = true; + public async discover(): Promise { + return new Promise((resolve) => { + this.discoveryInProgress = true; - this.browser = this.bonjour.find({ - type: 'hap' - }); + this.browser = this.bonjour.find({ + type: 'hap' + }); - // start matching services - this.browser.start(); - this.debug(`[HapClient] Discovery :: Started`); + // start matching services + this.browser.start(); + this.debug(`[HapClient] Discovery :: Started`); - // stop discovery after 20 seconds - setTimeout(() => { - if (this.browser) { - this.browser.stop(); - } - this.debug(`[HapClient] Discovery :: Ended`); - this.discoveryInProgress = false; - }, 60000); - - // service found - this.browser.on('up', async (service: any) => { - let device = service as IDevice; - if (!device || !device.txt) { - this.debug(`[HapClient] Discovery :: Ignoring device that contains no txt records. ${JSON.stringify(device)}`); - return; - } - - const instance: IHapInstance = { - displayName: device.name, - ipAddress: device.addresses[0], - name: device.txt.md, - username: device.txt.id, - port: device.port, - } - - this.debug(`[HapClient] Discovery :: Found HAP device ${instance.displayName} with username ${instance.username}`); - - // update an existing instance - const existingInstanceIndex = this.instances.findIndex(x => x.username === instance.username); - if (existingInstanceIndex > -1) { - - if ( - this.instances[existingInstanceIndex].port !== instance.port || - this.instances[existingInstanceIndex].name !== instance.name - ) { - this.instances[existingInstanceIndex].port = instance.port; - this.instances[existingInstanceIndex].name = instance.name; - this.debug(`[HapClient] Discovery :: [${this.instances[existingInstanceIndex].ipAddress}:${instance.port} ` + - `(${instance.username})] Instance Updated`); - this.emit('instance-discovered', instance); + // service found + this.browser.on('up', async (service: any) => { + let device = service as IDevice; + if (!device || !device.txt) { + this.debug(`[HapClient] Discovery :: Ignoring device that contains no txt records. ${JSON.stringify(device)}`); + return; } - return; - } + const instance: IHapInstance = { + displayName: device.name, + ipAddress: device.addresses[0], + name: device.txt.md, + username: device.txt.id, + port: device.port, + } - //Comenting out because of lack of config + this.debug(`[HapClient] Discovery :: Found HAP device ${instance.displayName} with username ${instance.username}`); - // check instance is not on the blacklist - // if (this.config.instanceBlacklist && this.config.instanceBlacklist.find(x => instance.username.toLowerCase() === x.toLowerCase())) { - // this.debug(`[HapClient] Discovery :: Instance with username ${instance.username} found in blacklist. Disregarding.`); - // return; - // } + // update an existing instance + const existingInstanceIndex = this.instances.findIndex(x => x.username === instance.username); + if (existingInstanceIndex > -1) { - for (const ip of device.addresses) { - if (ip.match(/^(?:(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(\.(?!$)|$)){4}$/)) { - try { - this.debug(`[HapClient] Discovery :: Testing ${instance.displayName} ${instance.username} via http://${ip}:${device.port}/accessories`); - const test = await get(`http://${ip}:${device.port}/accessories`, { - json: true, - timeout: 1000, - }); - if (test.accessories) { - this.debug(`[HapClient] Discovery :: Success ${instance.displayName} ${instance.username} via http://${ip}:${device.port}/accessories`); - instance.ipAddress = ip; + if ( + this.instances[existingInstanceIndex].port !== instance.port || + this.instances[existingInstanceIndex].name !== instance.name + ) { + this.instances[existingInstanceIndex].port = instance.port; + this.instances[existingInstanceIndex].name = instance.name; + this.debug(`[HapClient] Discovery :: [${this.instances[existingInstanceIndex].ipAddress}:${instance.port} ` + + `(${instance.username})] Instance Updated`); + } + + return; + } + + //Comenting out because of lack of config + + //check instance is not on the blacklist + if (this.config.instanceBlacklist && this.config.instanceBlacklist.find(x => instance.username.toLowerCase() === x.toLowerCase())) { + this.debug(`[HapClient] Discovery :: Instance with username ${instance.username} found in blacklist. Disregarding.`); + return; + } + + for (const ip of device.addresses) { + if (ip.match(/^(?:(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(\.(?!$)|$)){4}$/)) { + try { + this.debug(`[HapClient] Discovery :: Testing ${instance.displayName} ${instance.username} via http://${ip}:${device.port}/accessories`); + const test = await get(`http://${ip}:${device.port}/accessories`, { + json: true, + timeout: 1000, + headers: { Authorization: this.pin } + }); + if (test.accessories) { + this.debug(`[HapClient] Discovery :: Success ${instance.displayName} ${instance.username} via http://${ip}:${device.port}/accessories`); + instance.ipAddress = ip; + } + break; + } catch (e) { + this.debug(`[HapClient] Discovery :: ***Failed*** ${instance.displayName} ${instance.username} via http://${ip}:${device.port}/accessories: ${e.message}`); } - break; - } catch (e) { - this.debug(`[HapClient] Discovery :: ***Failed*** ${instance.displayName} ${instance.username} via http://${ip}:${device.port}/accessories`); } } - } - // store instance record - if (instance.ipAddress) { - this.instances.push(instance); - this.debug(`[HapClient] Discovery :: [${instance.displayName} - ${instance.ipAddress}:${instance.port} (${instance.username})] Instance Registered`); - this.emit('instance-discovered', instance); - } else { - this.debug(`[HapClient] Discovery :: Could not register to device ${instance.displayName} with username ${instance.username}`); - } + // store instance record + if (instance.ipAddress) { + this.instances.push(instance); + this.debug(`[HapClient] Discovery :: [${instance.displayName} - ${instance.ipAddress}:${instance.port} (${instance.username})] Instance Registered`); + } else { + this.debug(`[HapClient] Discovery :: Could not register to device ${instance.displayName} with username ${instance.username}`); + } + }); + + // stop discovery after 20 seconds + setTimeout(() => { + if (this.browser) { + this.browser.stop(); + } + this.debug(`[HapClient] Discovery :: Ended`); + this.discoveryInProgress = false; + return resolve(); + }, 20000); }); - } - private async getAccessories() { + private humanizeString(string: string) { + return inflection.titleize(decamelize(string)); + } + + public async getAccessories(): Promise> { if (!this.instances.length) { this.debug('[HapClient] Cannot load accessories. No Homebridge instances have been discovered.'); } - const accessories = []; + const accessories: Array = []; for (const instance of this.instances) { try { const resp: IHapAccessoriesRespType = await get(`http://${instance.ipAddress}:${instance.port}/accessories`, { json: true }); - for (const accessory of resp.accessories) { + resp.accessories && resp.accessories.forEach((accessory: IAccessoryResp) => { accessory.instance = instance; accessories.push(accessory); - } + }) } catch (e) { if (this.log) { this.debugErr(`[HapClient] [${instance.displayName} - ${instance.ipAddress}:${instance.port} (${instance.username})] Failed to connect`, e); @@ -337,17 +344,30 @@ export class HapClient extends EventEmitter { return services; } - async getService(iid: number) { - const services = await this.getAllServices(); - return services.find(x => x.iid === iid); + public getService(iid: number): Promise { + return new Promise(async (resolve, reject) => { + try { + const services = await this.getAllServices(); + return resolve(services.find(x => x.iid === iid)); + } catch (err) { + return reject(err); + } + }); + } - async getServiceByName(serviceName: string) { - const services = await this.getAllServices(); - return services.find(x => x.serviceName === serviceName); + public async getServiceByName(serviceName: string): Promise { + return new Promise(async (resolve, reject) => { + try { + const services = await this.getAllServices(); + return resolve(services.find(x => x.serviceName === serviceName)); + } catch (err) { + return reject(err); + } + }); } - async refreshServiceCharacteristics(service: IServiceType): Promise { + public async refreshServiceCharacteristics(service: IServiceType): Promise { const iids: number[] = service.serviceCharacteristics.map(c => c.iid); const resp = await get(`http://${service.instance.ipAddress}:${service.instance.port}/characteristics`, { @@ -357,15 +377,15 @@ export class HapClient extends EventEmitter { json: true }); - resp.characteristics.forEach((c: ICharacteristicType) => { - const characteristic = service.serviceCharacteristics.find(x => x.iid === c.iid && x.aid === service.aid); - characteristic!.value = c.value; + resp.characteristics.forEach((charType: ICharacteristicType) => { + const characteristic = service.serviceCharacteristics.find(x => x.iid === charType.iid && x.aid === service.aid); + characteristic!.value = charType.value; }); return service; } - async getCharacteristic(service: IServiceType, iid: number): Promise { + public async getCharacteristic(service: IServiceType, iid: number): Promise { const resp = await get(`http://${service.instance.ipAddress}:${service.instance.port}/characteristics`, { qs: { id: `${service.aid}.${iid}` @@ -381,7 +401,7 @@ export class HapClient extends EventEmitter { return characteristic ? characteristic : createDefaultCharacteristicType(); } - async setCharacteristic(service: IServiceType, iid: number, value: number | string | boolean): Promise { + public async setCharacteristic(service: IServiceType, iid: number, value: number | string | boolean): Promise { let characteristic = createDefaultCharacteristicType(); try { await put(`http://${service.instance.ipAddress}:${service.instance.port}/characteristics`, { @@ -417,9 +437,4 @@ export class HapClient extends EventEmitter { return characteristic } - - private humanizeString(string: string) { - return inflection.titleize(decamelize(string)); - } - } diff --git a/src/3rdParty/HapClient/interfaces.ts b/src/3rdParty/HapClient/interfaces.ts index 04b937b..8e6e5ff 100755 --- a/src/3rdParty/HapClient/interfaces.ts +++ b/src/3rdParty/HapClient/interfaces.ts @@ -17,35 +17,43 @@ export interface IHapEvInstance { socket?: Socket; } +export interface IInstanceResp { + ipAddress: string; + port: number; + username: string; + name: string; +} + +export interface ICharacteristicResp { + iid: number; + type: string; + description: string; + value: number | string | boolean; + format: 'bool' | 'int' | 'float' | 'string' | 'uint8' | 'uint16' | 'uint32' | 'uint64' | 'data' | 'tlv8' | 'array' | 'dictionary'; + perms: Array<'pr' | 'pw' | 'ev' | 'aa' | 'tw' | 'hd'>; + unit?: 'unit' | 'percentage' | 'celsius' | 'arcdegrees' | 'lux' | 'seconds'; + maxValue?: number; + minValue?: number; + minStep?: number; +} + +export interface IServiceResp { + iid: number; + type: string; + primary: boolean; + hidden: boolean; + linked?: Array; + characteristics: Array; +} + +export interface IAccessoryResp { + instance: IInstanceResp + aid: number; + services: Array; +} + export interface IHapAccessoriesRespType { - accessories: Array<{ - instance: { - ipAddress: string; - port: number; - username: string; - name: string; - }; - aid: number; - services: Array<{ - iid: number; - type: string; - primary: boolean; - hidden: boolean; - linked?: Array; - characteristics: Array<{ - iid: number; - type: string; - description: string; - value: number | string | boolean; - format: 'bool' | 'int' | 'float' | 'string' | 'uint8' | 'uint16' | 'uint32' | 'uint64' | 'data' | 'tlv8' | 'array' | 'dictionary'; - perms: Array<'pr' | 'pw' | 'ev' | 'aa' | 'tw' | 'hd'>; - unit?: 'unit' | 'percentage' | 'celsius' | 'arcdegrees' | 'lux' | 'seconds'; - maxValue?: number; - minValue?: number; - minStep?: number; - }>; - }>; - }>; + accessories: Array; } export interface IServiceType { diff --git a/src/3rdParty/HapClient/monitor.ts b/src/3rdParty/HapClient/monitor.ts index b8de152..3509094 100755 --- a/src/3rdParty/HapClient/monitor.ts +++ b/src/3rdParty/HapClient/monitor.ts @@ -1,7 +1,7 @@ import { EventEmitter } from 'events'; import { IServiceType, IHapEvInstance } from './interfaces'; import { createConnection, parseMessage } from './eventedHttpClient'; -import { CharacteristicType } from '.'; +import { CharacteristicType } from './hapClient'; export class HapMonitor extends EventEmitter { private pin: string; @@ -25,7 +25,10 @@ export class HapMonitor extends EventEmitter { this.start(); } - start() { + /** + * Start monitoring + */ + public start() { for (const instance of this.evInstances) { instance.socket = createConnection(instance, this.pin, { characteristics: instance.evCharacteristics }); @@ -78,7 +81,10 @@ export class HapMonitor extends EventEmitter { } } - finish() { + /** + * Stop monitoring. + */ + public stop() { for (const instance of this.evInstances) { if (instance.socket) { try { @@ -91,7 +97,7 @@ export class HapMonitor extends EventEmitter { } } - parseServices() { + private parseServices() { // get a list of characteristics we can watch for each instance for (const service of this.services) { const evCharacteristics = service.serviceCharacteristics.filter(x => x.perms.includes('ev')); diff --git a/src/index.ts b/src/index.ts index 4db7dac..2b07647 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { HapClient } from "./3rdParty/HapClient"; +import { HapClient } from "./3rdParty/HapClient/hapClient"; import { HapMonitor } from "./3rdParty/HapClient/monitor"; let Accessory: any; @@ -38,9 +38,9 @@ class AutomationPlatform { config: config }); - this.client.on('instance-discovered', async (instance: any) => { - let asdf = instance; - + this.client.discover().then(async () => { + let asdf = await this.client.getAccessories(); + let asdff = "asdf"; }) }