Code cleanup with stronger type checking

This commit is contained in:
watsonb8 2019-09-07 18:04:09 -04:00
parent 498b59bfef
commit 25ae519d3d
5 changed files with 194 additions and 151 deletions

View File

@ -8,10 +8,24 @@ import * as url from 'url';
import httpMessageParser from './/httpParser'; import httpMessageParser from './/httpParser';
import { Dictionary } from '../../../Types/types'; import { Dictionary } from '../../../Types/types';
interface IRequest {
method: 'PUT' | 'POST' | 'GET' | 'DELETE' | 'PATCH';
url: string;
maxAttempts: number;
headers: Dictionary<string>;
body: string;
}
export const parseMessage = httpMessageParser; 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, host: instance.ipAddress,
port: instance.port, port: instance.port,
}); });
@ -40,7 +54,7 @@ function _headersToString(headers: Dictionary<string>) {
return (response); return (response);
} }
function _buildMessage(request: any) { function _buildMessage(request: IRequest) {
const context = url.parse(request.url); const context = url.parse(request.url);
let message; let message;

View File

@ -8,7 +8,7 @@ import { get, put } from 'request-promise-native';
import { Services, Characteristics } from './hap-types'; import { Services, Characteristics } from './hap-types';
import { HapMonitor } from './monitor'; 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 HapAccessoriesRespType = IHapAccessoriesRespType;
export type ServiceType = IServiceType; export type ServiceType = IServiceType;
@ -33,7 +33,12 @@ export interface IDevice {
type: string type: string
} }
export class HapClient extends EventEmitter { interface IConfig {
debug?: boolean;
instanceBlacklist?: string[];
}
export class HapClient {
private bonjour = Bonjour(); private bonjour = Bonjour();
private browser?: Bonjour.Browser; private browser?: Bonjour.Browser;
private discoveryInProgress = false; private discoveryInProgress = false;
@ -41,10 +46,10 @@ export class HapClient extends EventEmitter {
private log: (msg: string) => void; private log: (msg: string) => void;
private pin: string; private pin: string;
private debugEnabled: boolean; private debugEnabled: boolean;
private config: { private config: IConfig = {
debug?: boolean; debug: true,
instanceBlacklist?: string[]; instanceBlacklist: [],
}; }
private instances: IHapInstance[] = []; private instances: IHapInstance[] = [];
@ -61,13 +66,10 @@ export class HapClient extends EventEmitter {
logger?: any; logger?: any;
config: any; config: any;
}) { }) {
super();
this.pin = opts.pin; this.pin = opts.pin;
this.log = opts.logger; this.log = opts.logger;
this.debugEnabled = true; this.debugEnabled = true;
this.config = opts.config; this.config = opts.config;
this.startDiscovery();
} }
private debug(msg: any) { private debug(msg: any) {
@ -84,7 +86,7 @@ export class HapClient extends EventEmitter {
public refreshInstances() { public refreshInstances() {
if (!this.discoveryInProgress) { if (!this.discoveryInProgress) {
this.startDiscovery(); this.discover();
} else { } else {
try { try {
this.debug(`[HapClient] Discovery :: Re-broadcasting discovery query`); this.debug(`[HapClient] Discovery :: Re-broadcasting discovery query`);
@ -92,118 +94,123 @@ export class HapClient extends EventEmitter {
this.browser.update(); this.browser.update();
} }
} catch (e) { } } catch{ }
} }
} }
private async startDiscovery() { public async discover(): Promise<void> {
this.discoveryInProgress = true; return new Promise((resolve) => {
this.discoveryInProgress = true;
this.browser = this.bonjour.find({ this.browser = this.bonjour.find({
type: 'hap' type: 'hap'
}); });
// start matching services // start matching services
this.browser.start(); this.browser.start();
this.debug(`[HapClient] Discovery :: Started`); this.debug(`[HapClient] Discovery :: Started`);
// stop discovery after 20 seconds // service found
setTimeout(() => { this.browser.on('up', async (service: any) => {
if (this.browser) { let device = service as IDevice;
this.browser.stop(); if (!device || !device.txt) {
} this.debug(`[HapClient] Discovery :: Ignoring device that contains no txt records. ${JSON.stringify(device)}`);
this.debug(`[HapClient] Discovery :: Ended`); return;
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);
} }
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 // update an existing instance
// if (this.config.instanceBlacklist && this.config.instanceBlacklist.find(x => instance.username.toLowerCase() === x.toLowerCase())) { const existingInstanceIndex = this.instances.findIndex(x => x.username === instance.username);
// this.debug(`[HapClient] Discovery :: Instance with username ${instance.username} found in blacklist. Disregarding.`); if (existingInstanceIndex > -1) {
// return;
// }
for (const ip of device.addresses) { if (
if (ip.match(/^(?:(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(\.(?!$)|$)){4}$/)) { this.instances[existingInstanceIndex].port !== instance.port ||
try { this.instances[existingInstanceIndex].name !== instance.name
this.debug(`[HapClient] Discovery :: Testing ${instance.displayName} ${instance.username} via http://${ip}:${device.port}/accessories`); ) {
const test = await get(`http://${ip}:${device.port}/accessories`, { this.instances[existingInstanceIndex].port = instance.port;
json: true, this.instances[existingInstanceIndex].name = instance.name;
timeout: 1000, this.debug(`[HapClient] Discovery :: [${this.instances[existingInstanceIndex].ipAddress}:${instance.port} ` +
}); `(${instance.username})] Instance Updated`);
if (test.accessories) { }
this.debug(`[HapClient] Discovery :: Success ${instance.displayName} ${instance.username} via http://${ip}:${device.port}/accessories`);
instance.ipAddress = ip; 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 // store instance record
if (instance.ipAddress) { if (instance.ipAddress) {
this.instances.push(instance); this.instances.push(instance);
this.debug(`[HapClient] Discovery :: [${instance.displayName} - ${instance.ipAddress}:${instance.port} (${instance.username})] Instance Registered`); this.debug(`[HapClient] Discovery :: [${instance.displayName} - ${instance.ipAddress}:${instance.port} (${instance.username})] Instance Registered`);
this.emit('instance-discovered', instance); } else {
} else { this.debug(`[HapClient] Discovery :: Could not register to device ${instance.displayName} with username ${instance.username}`);
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<Array<IAccessoryResp>> {
if (!this.instances.length) { if (!this.instances.length) {
this.debug('[HapClient] Cannot load accessories. No Homebridge instances have been discovered.'); this.debug('[HapClient] Cannot load accessories. No Homebridge instances have been discovered.');
} }
const accessories = []; const accessories: Array<IAccessoryResp> = [];
for (const instance of this.instances) { for (const instance of this.instances) {
try { try {
const resp: IHapAccessoriesRespType = await get(`http://${instance.ipAddress}:${instance.port}/accessories`, { json: true }); 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; accessory.instance = instance;
accessories.push(accessory); accessories.push(accessory);
} })
} catch (e) { } catch (e) {
if (this.log) { if (this.log) {
this.debugErr(`[HapClient] [${instance.displayName} - ${instance.ipAddress}:${instance.port} (${instance.username})] Failed to connect`, e); 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; return services;
} }
async getService(iid: number) { public getService(iid: number): Promise<IServiceType | undefined> {
const services = await this.getAllServices(); return new Promise(async (resolve, reject) => {
return services.find(x => x.iid === iid); try {
const services = await this.getAllServices();
return resolve(services.find(x => x.iid === iid));
} catch (err) {
return reject(err);
}
});
} }
async getServiceByName(serviceName: string) { public async getServiceByName(serviceName: string): Promise<IServiceType | undefined> {
const services = await this.getAllServices(); return new Promise(async (resolve, reject) => {
return services.find(x => x.serviceName === serviceName); try {
const services = await this.getAllServices();
return resolve(services.find(x => x.serviceName === serviceName));
} catch (err) {
return reject(err);
}
});
} }
async refreshServiceCharacteristics(service: IServiceType): Promise<IServiceType> { public async refreshServiceCharacteristics(service: IServiceType): Promise<IServiceType> {
const iids: number[] = service.serviceCharacteristics.map(c => c.iid); const iids: number[] = service.serviceCharacteristics.map(c => c.iid);
const resp = await get(`http://${service.instance.ipAddress}:${service.instance.port}/characteristics`, { const resp = await get(`http://${service.instance.ipAddress}:${service.instance.port}/characteristics`, {
@ -357,15 +377,15 @@ export class HapClient extends EventEmitter {
json: true json: true
}); });
resp.characteristics.forEach((c: ICharacteristicType) => { resp.characteristics.forEach((charType: ICharacteristicType) => {
const characteristic = service.serviceCharacteristics.find(x => x.iid === c.iid && x.aid === service.aid); const characteristic = service.serviceCharacteristics.find(x => x.iid === charType.iid && x.aid === service.aid);
characteristic!.value = c.value; characteristic!.value = charType.value;
}); });
return service; return service;
} }
async getCharacteristic(service: IServiceType, iid: number): Promise<ICharacteristicType> { public async getCharacteristic(service: IServiceType, iid: number): Promise<ICharacteristicType> {
const resp = await get(`http://${service.instance.ipAddress}:${service.instance.port}/characteristics`, { const resp = await get(`http://${service.instance.ipAddress}:${service.instance.port}/characteristics`, {
qs: { qs: {
id: `${service.aid}.${iid}` id: `${service.aid}.${iid}`
@ -381,7 +401,7 @@ export class HapClient extends EventEmitter {
return characteristic ? characteristic : createDefaultCharacteristicType(); return characteristic ? characteristic : createDefaultCharacteristicType();
} }
async setCharacteristic(service: IServiceType, iid: number, value: number | string | boolean): Promise<ICharacteristicType> { public async setCharacteristic(service: IServiceType, iid: number, value: number | string | boolean): Promise<ICharacteristicType> {
let characteristic = createDefaultCharacteristicType(); let characteristic = createDefaultCharacteristicType();
try { try {
await put(`http://${service.instance.ipAddress}:${service.instance.port}/characteristics`, { await put(`http://${service.instance.ipAddress}:${service.instance.port}/characteristics`, {
@ -417,9 +437,4 @@ export class HapClient extends EventEmitter {
return characteristic return characteristic
} }
private humanizeString(string: string) {
return inflection.titleize(decamelize(string));
}
} }

View File

@ -17,35 +17,43 @@ export interface IHapEvInstance {
socket?: Socket; 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<number>;
characteristics: Array<ICharacteristicResp>;
}
export interface IAccessoryResp {
instance: IInstanceResp
aid: number;
services: Array<IServiceResp>;
}
export interface IHapAccessoriesRespType { export interface IHapAccessoriesRespType {
accessories: Array<{ accessories: Array<IAccessoryResp>;
instance: {
ipAddress: string;
port: number;
username: string;
name: string;
};
aid: number;
services: Array<{
iid: number;
type: string;
primary: boolean;
hidden: boolean;
linked?: Array<number>;
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;
}>;
}>;
}>;
} }
export interface IServiceType { export interface IServiceType {

View File

@ -1,7 +1,7 @@
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { IServiceType, IHapEvInstance } from './interfaces'; import { IServiceType, IHapEvInstance } from './interfaces';
import { createConnection, parseMessage } from './eventedHttpClient'; import { createConnection, parseMessage } from './eventedHttpClient';
import { CharacteristicType } from '.'; import { CharacteristicType } from './hapClient';
export class HapMonitor extends EventEmitter { export class HapMonitor extends EventEmitter {
private pin: string; private pin: string;
@ -25,7 +25,10 @@ export class HapMonitor extends EventEmitter {
this.start(); this.start();
} }
start() { /**
* Start monitoring
*/
public start() {
for (const instance of this.evInstances) { for (const instance of this.evInstances) {
instance.socket = createConnection(instance, this.pin, { characteristics: instance.evCharacteristics }); 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) { for (const instance of this.evInstances) {
if (instance.socket) { if (instance.socket) {
try { try {
@ -91,7 +97,7 @@ export class HapMonitor extends EventEmitter {
} }
} }
parseServices() { private parseServices() {
// get a list of characteristics we can watch for each instance // get a list of characteristics we can watch for each instance
for (const service of this.services) { for (const service of this.services) {
const evCharacteristics = service.serviceCharacteristics.filter(x => x.perms.includes('ev')); const evCharacteristics = service.serviceCharacteristics.filter(x => x.perms.includes('ev'));

View File

@ -1,4 +1,4 @@
import { HapClient } from "./3rdParty/HapClient"; import { HapClient } from "./3rdParty/HapClient/hapClient";
import { HapMonitor } from "./3rdParty/HapClient/monitor"; import { HapMonitor } from "./3rdParty/HapClient/monitor";
let Accessory: any; let Accessory: any;
@ -38,9 +38,9 @@ class AutomationPlatform {
config: config config: config
}); });
this.client.on('instance-discovered', async (instance: any) => { this.client.discover().then(async () => {
let asdf = instance; let asdf = await this.client.getAccessories();
let asdff = "asdf";
}) })
} }