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 { 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 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<string>) {
return (response);
}
function _buildMessage(request: any) {
function _buildMessage(request: IRequest) {
const context = url.parse(request.url);
let message;

View File

@ -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<void> {
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<Array<IAccessoryResp>> {
if (!this.instances.length) {
this.debug('[HapClient] Cannot load accessories. No Homebridge instances have been discovered.');
}
const accessories = [];
const accessories: Array<IAccessoryResp> = [];
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<IServiceType | undefined> {
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<IServiceType | undefined> {
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<IServiceType> {
public async refreshServiceCharacteristics(service: IServiceType): Promise<IServiceType> {
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<ICharacteristicType> {
public async getCharacteristic(service: IServiceType, iid: number): Promise<ICharacteristicType> {
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<ICharacteristicType> {
public async setCharacteristic(service: IServiceType, iid: number, value: number | string | boolean): Promise<ICharacteristicType> {
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));
}
}

View File

@ -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<number>;
characteristics: Array<ICharacteristicResp>;
}
export interface IAccessoryResp {
instance: IInstanceResp
aid: number;
services: Array<IServiceResp>;
}
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<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;
}>;
}>;
}>;
accessories: Array<IAccessoryResp>;
}
export interface IServiceType {

View File

@ -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'));

View File

@ -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";
})
}