Integrated oznu's hapclient library.

This commit is contained in:
watsonb8
2019-09-02 21:39:08 -04:00
parent b20d657674
commit 498b59bfef
10 changed files with 2068 additions and 4 deletions

View File

@ -0,0 +1,298 @@
/**
* The contents in this file were taken from NorthernMan54/Hap-Node-Client
* https://raw.githubusercontent.com/NorthernMan54/Hap-Node-Client/master/lib/httpParser.js
*/
// Borrowed and heaviliy modifed from https://github.com/miguelmota/http-message-parser
import { Buffer } from 'buffer';
import { Dictionary } from '../../../Types/types';
interface httpResult {
protocol?: string,
httpVersion?: number,
statusCode?: number,
statusMessage?: string,
method?: string,
url?: string,
headers?: Dictionary<string | number>,
body?: any,
boundary?: any,
multipart?: any,
additional?: any,
meta?: any
}
function httpMessageParser(message: any) {
const result: httpResult = {}
let messageString = '';
let headerNewlineIndex = 0;
let fullBoundary: any = null;
if (httpMessageParser._isBuffer(message)) {
messageString = message.toString();
} else if (typeof message === 'string') {
messageString = message;
message = new Buffer(messageString);
} else {
return result;
}
/*
* Strip extra return characters
*/
messageString = messageString.replace(/\r\n/gim, '\n');
/*
* Trim leading whitespace
*/
(function () {
const firstNonWhitespaceRegex = /[\w-]+/gim;
const firstNonWhitespaceIndex = messageString.search(firstNonWhitespaceRegex);
if (firstNonWhitespaceIndex > 0) {
message = message.slice(firstNonWhitespaceIndex, message.length);
messageString = message.toString();
}
})();
/* Parse request line
*/
(function () {
const possibleRequestLine = messageString.split(/\n|\r\n/)[0];
const requestLineMatch = possibleRequestLine.match(httpMessageParser._requestLineRegex);
if (Array.isArray(requestLineMatch) && requestLineMatch.length > 1) {
result.protocol = requestLineMatch[1];
result.httpVersion = parseFloat(requestLineMatch[2]);
result.statusCode = parseInt(requestLineMatch[3], 10);
result.statusMessage = requestLineMatch[4];
} else {
const responseLineMath = possibleRequestLine.match(httpMessageParser._responseLineRegex);
if (Array.isArray(responseLineMath) && responseLineMath.length > 1) {
result.method = responseLineMath[1];
result.url = responseLineMath[2];
result.httpVersion = parseFloat(responseLineMath[3]);
}
}
})();
/* Parse headers
*/
(function () {
headerNewlineIndex = messageString.search(httpMessageParser._headerNewlineRegex);
if (headerNewlineIndex > -1) {
headerNewlineIndex = headerNewlineIndex + 1; // 1 for newline length
} else {
/* There's no line breaks so check if request line exists
* because the message might be all headers and no body
*/
if (result.httpVersion) {
headerNewlineIndex = messageString.length;
}
}
const headersString = messageString.substr(0, headerNewlineIndex);
const headers = httpMessageParser._parseHeaders(headersString);
if (Object.keys(headers).length > 0) {
result.headers = headers;
// TOOD: extract boundary.
}
})();
/* Try to get boundary if no boundary header
*/
(function () {
if (!result.boundary) {
const boundaryMatch = messageString.match(httpMessageParser._boundaryRegex);
if (Array.isArray(boundaryMatch) && boundaryMatch.length) {
fullBoundary = boundaryMatch[0].replace(/[\r\n]+/gi, '');
const boundary = fullBoundary.replace(/^--/, '');
result.boundary = boundary;
}
}
})();
/* Parse body
*/
(function () {
let start = headerNewlineIndex;
let contentLength: number = result.headers!['Content-Length'] as number;
let end = (result.headers && result.headers['Content-Length'] && contentLength ? contentLength + start : messageString.length);
const firstBoundaryIndex = messageString.indexOf(fullBoundary);
if (firstBoundaryIndex > -1 && result.boundary) {
start = headerNewlineIndex;
end = firstBoundaryIndex;
}
if (headerNewlineIndex > -1) {
const body = messageString.slice(start, end);
result.additional = messageString.slice(end); // Pass back any unparsed data for running thru again
// console.log("Lengths: total %s -> start %s -> end %s -> final %s", messageString.length, start, end, body.length);
if (body && body.length) {
if ((result.headers && result.headers['Content-Type'] === 'application/hap+json') ||
(result.headers && result.headers['Content-Type'] === 'application/json')) {
// JSON.parse JSON message's
try {
if (result.headers['Content-Length']) {
result.body = body;
} else {
result.body = body.split('\n')[1];
}
} catch (err) {
}
} else {
result.body = body;
}
}
}
})();
/* Parse multipart sections
*/
(function () {
if (result.boundary) {
const multipartStart = messageString.indexOf(fullBoundary) + fullBoundary.length;
const multipartEnd = messageString.lastIndexOf(fullBoundary);
const multipartBody = messageString.substr(multipartStart, multipartEnd);
const splitRegex = new RegExp('^' + fullBoundary + '.*[\n\r]?$', 'gm');
const parts = multipartBody.split(splitRegex);
result.multipart = parts.filter(httpMessageParser._isTruthy).map(function (part, i) {
// tslint:disable-next-line: no-shadowed-variable
const result: httpResult = {};
const newlineRegex = /\n\n|\r\n\r\n/gim;
let newlineIndex = 0;
let newlineMatch = newlineRegex.exec(part);
let body = null;
if (newlineMatch) {
newlineIndex = newlineMatch.index;
if (newlineMatch.index <= 0) {
newlineMatch = newlineRegex.exec(part);
if (newlineMatch) {
newlineIndex = newlineMatch.index;
}
}
}
const possibleHeadersString = part.substr(0, newlineIndex);
let startOffset = null;
let endOffset = null;
if (newlineIndex > -1) {
const headers = httpMessageParser._parseHeaders(possibleHeadersString);
if (Object.keys(headers).length > 0) {
result.headers = headers;
const boundaryIndexes: Array<number> = [];
for (let j = 0; j >= 0;) {
j = message.indexOf(fullBoundary, j);
if (j >= 0) {
boundaryIndexes.push(j);
j += fullBoundary.length;
}
}
const boundaryNewlineIndexes: Array<number> = [];
boundaryIndexes.slice(0, boundaryIndexes.length - 1).forEach(function (m, k) {
const partBody = message.slice(boundaryIndexes[k], boundaryIndexes[k + 1]).toString();
// tslint:disable-next-line: no-shadowed-variable
let headerNewlineIndex = partBody.search(/\n\n|\r\n\r\n/gim) + 2;
headerNewlineIndex = boundaryIndexes[k] + headerNewlineIndex;
boundaryNewlineIndexes.push(headerNewlineIndex);
});
startOffset = boundaryNewlineIndexes[i];
endOffset = boundaryIndexes[i + 1];
body = message.slice(startOffset, endOffset);
} else {
body = part;
}
} else {
body = part;
}
result.body = body;
result.meta.body.byteOffset.start = startOffset;
result.meta.body.byteOffset.end = endOffset;
return result;
});
}
})();
return result;
}
httpMessageParser._isTruthy = function _isTruthy(value: any) {
return !!value;
};
httpMessageParser._isNumeric = function _isNumeric(value: any) {
if (typeof value === 'number' && !isNaN(value)) {
return true;
}
value = (value || '').toString().trim();
if (!value) {
return false;
}
return !isNaN(value);
};
httpMessageParser._isBuffer = function (item: any) {
return ((httpMessageParser._isNodeBufferSupported() &&
typeof global === 'object' &&
global.Buffer.isBuffer(item)) ||
(item instanceof Object &&
item._isBuffer));
};
httpMessageParser._isNodeBufferSupported = function () {
return (typeof global === 'object' &&
typeof global.Buffer === 'function' &&
typeof global.Buffer.isBuffer === 'function');
};
httpMessageParser._parseHeaders = function _parseHeaders(body: any) {
const headers: Dictionary<string | number> = {};
if (typeof body !== 'string') {
return headers;
}
body.split(/[\r\n]/).forEach(function (string) {
const match = string.match(/([\w-]+):\s*(.*)/i);
if (Array.isArray(match) && match.length === 3) {
const key = match[1];
const value = match[2];
headers[key] = httpMessageParser._isNumeric(value) ? Number(value) : value;
}
});
return headers;
};
httpMessageParser._requestLineRegex = /(HTTP|EVENT)\/(1\.0|1\.1|2\.0)\s+(\d+)\s+([\w\s-_]+)/i;
httpMessageParser._responseLineRegex = /(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD|TRACE|CONNECT)\s+(.*)\s+HTTP\/(1\.0|1\.1|2\.0)/i;
// httpMessageParser._headerNewlineRegex = /^[\r\n]+/gim;
httpMessageParser._headerNewlineRegex = /^[\r\n]+/gim;
httpMessageParser._boundaryRegex = /(\n|\r\n)+--[\w-]+(\n|\r\n)+/g;
export default httpMessageParser;

View File

@ -0,0 +1,59 @@
/**
* The contents in this file were taken from NorthernMan54/Hap-Node-Client
* https://github.com/NorthernMan54/Hap-Node-Client/blob/master/lib/eventedHttpClient.js
*/
import * as net from 'net';
import * as url from 'url';
import httpMessageParser from './/httpParser';
import { Dictionary } from '../../../Types/types';
export const parseMessage = httpMessageParser;
export function createConnection(instance: { ipAddress: string, port: number }, pin: string, body: any) {
const client = net.createConnection({
host: instance.ipAddress,
port: instance.port,
});
client.write(_buildMessage({
method: 'PUT',
url: 'http://' + instance.ipAddress + ':' + instance.port + '/characteristics',
maxAttempts: 1, // (default) try 5 times
headers: {
'Content-Type': 'Application/json',
'authorization': pin,
'connection': 'keep-alive'
},
body: JSON.stringify(body)
}));
return client;
}
function _headersToString(headers: Dictionary<string>) {
let response = '';
for (const header of Object.keys(headers)) {
response = response + header + ': ' + headers[header] + '\r\n';
}
return (response);
}
function _buildMessage(request: any) {
const context = url.parse(request.url);
let message;
message = request.method + ' ' + context.pathname;
if (context.search) {
message = message + context.search;
}
message = message + ' HTTP/1.1\r\nHost: ' + context.host + '\r\n' + _headersToString(request.headers);
if (request.body) {
message = message + 'Content-Length: ' + request.body.length + '\r\n\r\n' + request.body + '\r\n\r\n';
} else {
message = message + '\r\n\r\n';
}
// debug("Message ->", message);
return (message);
}

378
src/3rdParty/HapClient/hap-types.ts vendored Executable file
View File

@ -0,0 +1,378 @@
import { Dictionary } from "../../Types/types";
/* This file is automatically generated */
export const Services: Dictionary<string> = {
'0000003E-0000-1000-8000-0026BB765291': 'AccessoryInformation',
'AccessoryInformation': '0000003E-0000-1000-8000-0026BB765291',
'000000BB-0000-1000-8000-0026BB765291': 'AirPurifier',
'AirPurifier': '000000BB-0000-1000-8000-0026BB765291',
'0000008D-0000-1000-8000-0026BB765291': 'AirQualitySensor',
'AirQualitySensor': '0000008D-0000-1000-8000-0026BB765291',
'00000096-0000-1000-8000-0026BB765291': 'BatteryService',
'BatteryService': '00000096-0000-1000-8000-0026BB765291',
'00000110-0000-1000-8000-0026BB765291': 'CameraRTPStreamManagement',
'CameraRTPStreamManagement': '00000110-0000-1000-8000-0026BB765291',
'00000097-0000-1000-8000-0026BB765291': 'CarbonDioxideSensor',
'CarbonDioxideSensor': '00000097-0000-1000-8000-0026BB765291',
'0000007F-0000-1000-8000-0026BB765291': 'CarbonMonoxideSensor',
'CarbonMonoxideSensor': '0000007F-0000-1000-8000-0026BB765291',
'00000080-0000-1000-8000-0026BB765291': 'ContactSensor',
'ContactSensor': '00000080-0000-1000-8000-0026BB765291',
'00000081-0000-1000-8000-0026BB765291': 'Door',
'Door': '00000081-0000-1000-8000-0026BB765291',
'00000121-0000-1000-8000-0026BB765291': 'Doorbell',
'Doorbell': '00000121-0000-1000-8000-0026BB765291',
'00000040-0000-1000-8000-0026BB765291': 'Fan',
'Fan': '00000040-0000-1000-8000-0026BB765291',
'000000B7-0000-1000-8000-0026BB765291': 'Fanv2',
'Fanv2': '000000B7-0000-1000-8000-0026BB765291',
'000000BA-0000-1000-8000-0026BB765291': 'FilterMaintenance',
'FilterMaintenance': '000000BA-0000-1000-8000-0026BB765291',
'000000D7-0000-1000-8000-0026BB765291': 'Faucet',
'Faucet': '000000D7-0000-1000-8000-0026BB765291',
'00000041-0000-1000-8000-0026BB765291': 'GarageDoorOpener',
'GarageDoorOpener': '00000041-0000-1000-8000-0026BB765291',
'000000BC-0000-1000-8000-0026BB765291': 'HeaterCooler',
'HeaterCooler': '000000BC-0000-1000-8000-0026BB765291',
'000000BD-0000-1000-8000-0026BB765291': 'HumidifierDehumidifier',
'HumidifierDehumidifier': '000000BD-0000-1000-8000-0026BB765291',
'00000082-0000-1000-8000-0026BB765291': 'HumiditySensor',
'HumiditySensor': '00000082-0000-1000-8000-0026BB765291',
'000000CF-0000-1000-8000-0026BB765291': 'IrrigationSystem',
'IrrigationSystem': '000000CF-0000-1000-8000-0026BB765291',
'00000083-0000-1000-8000-0026BB765291': 'LeakSensor',
'LeakSensor': '00000083-0000-1000-8000-0026BB765291',
'00000084-0000-1000-8000-0026BB765291': 'LightSensor',
'LightSensor': '00000084-0000-1000-8000-0026BB765291',
'00000043-0000-1000-8000-0026BB765291': 'Lightbulb',
'Lightbulb': '00000043-0000-1000-8000-0026BB765291',
'00000044-0000-1000-8000-0026BB765291': 'LockManagement',
'LockManagement': '00000044-0000-1000-8000-0026BB765291',
'00000045-0000-1000-8000-0026BB765291': 'LockMechanism',
'LockMechanism': '00000045-0000-1000-8000-0026BB765291',
'00000112-0000-1000-8000-0026BB765291': 'Microphone',
'Microphone': '00000112-0000-1000-8000-0026BB765291',
'00000085-0000-1000-8000-0026BB765291': 'MotionSensor',
'MotionSensor': '00000085-0000-1000-8000-0026BB765291',
'00000086-0000-1000-8000-0026BB765291': 'OccupancySensor',
'OccupancySensor': '00000086-0000-1000-8000-0026BB765291',
'00000047-0000-1000-8000-0026BB765291': 'Outlet',
'Outlet': '00000047-0000-1000-8000-0026BB765291',
'0000007E-0000-1000-8000-0026BB765291': 'SecuritySystem',
'SecuritySystem': '0000007E-0000-1000-8000-0026BB765291',
'000000CC-0000-1000-8000-0026BB765291': 'ServiceLabel',
'ServiceLabel': '000000CC-0000-1000-8000-0026BB765291',
'000000B9-0000-1000-8000-0026BB765291': 'Slat',
'Slat': '000000B9-0000-1000-8000-0026BB765291',
'00000087-0000-1000-8000-0026BB765291': 'SmokeSensor',
'SmokeSensor': '00000087-0000-1000-8000-0026BB765291',
'00000113-0000-1000-8000-0026BB765291': 'Speaker',
'Speaker': '00000113-0000-1000-8000-0026BB765291',
'00000089-0000-1000-8000-0026BB765291': 'StatelessProgrammableSwitch',
'StatelessProgrammableSwitch': '00000089-0000-1000-8000-0026BB765291',
'00000049-0000-1000-8000-0026BB765291': 'Switch',
'Switch': '00000049-0000-1000-8000-0026BB765291',
'0000008A-0000-1000-8000-0026BB765291': 'TemperatureSensor',
'TemperatureSensor': '0000008A-0000-1000-8000-0026BB765291',
'0000004A-0000-1000-8000-0026BB765291': 'Thermostat',
'Thermostat': '0000004A-0000-1000-8000-0026BB765291',
'000000D0-0000-1000-8000-0026BB765291': 'Valve',
'Valve': '000000D0-0000-1000-8000-0026BB765291',
'0000008B-0000-1000-8000-0026BB765291': 'Window',
'Window': '0000008B-0000-1000-8000-0026BB765291',
'0000008C-0000-1000-8000-0026BB765291': 'WindowCovering',
'WindowCovering': '0000008C-0000-1000-8000-0026BB765291',
'000000D8-0000-1000-8000-0026BB765291': 'Television',
'Television': '000000D8-0000-1000-8000-0026BB765291',
'000000D9-0000-1000-8000-0026BB765291': 'InputSource',
'InputSource': '000000D9-0000-1000-8000-0026BB765291',
'TelevisionSpeaker': '00000113-0000-1000-8000-0026BB765291',
};
export const Characteristics: Dictionary<string> = {
'000000A6-0000-1000-8000-0026BB765291': 'AccessoryFlags',
'AccessoryFlags': '000000A6-0000-1000-8000-0026BB765291',
'000000B0-0000-1000-8000-0026BB765291': 'Active',
'Active': '000000B0-0000-1000-8000-0026BB765291',
'00000001-0000-1000-8000-0026BB765291': 'AdministratorOnlyAccess',
'AdministratorOnlyAccess': '00000001-0000-1000-8000-0026BB765291',
'00000064-0000-1000-8000-0026BB765291': 'AirParticulateDensity',
'AirParticulateDensity': '00000064-0000-1000-8000-0026BB765291',
'00000065-0000-1000-8000-0026BB765291': 'AirParticulateSize',
'AirParticulateSize': '00000065-0000-1000-8000-0026BB765291',
'00000095-0000-1000-8000-0026BB765291': 'AirQuality',
'AirQuality': '00000095-0000-1000-8000-0026BB765291',
'00000005-0000-1000-8000-0026BB765291': 'AudioFeedback',
'AudioFeedback': '00000005-0000-1000-8000-0026BB765291',
'00000068-0000-1000-8000-0026BB765291': 'BatteryLevel',
'BatteryLevel': '00000068-0000-1000-8000-0026BB765291',
'00000008-0000-1000-8000-0026BB765291': 'Brightness',
'Brightness': '00000008-0000-1000-8000-0026BB765291',
'00000092-0000-1000-8000-0026BB765291': 'CarbonDioxideDetected',
'CarbonDioxideDetected': '00000092-0000-1000-8000-0026BB765291',
'00000093-0000-1000-8000-0026BB765291': 'CarbonDioxideLevel',
'CarbonDioxideLevel': '00000093-0000-1000-8000-0026BB765291',
'00000094-0000-1000-8000-0026BB765291': 'CarbonDioxidePeakLevel',
'CarbonDioxidePeakLevel': '00000094-0000-1000-8000-0026BB765291',
'00000069-0000-1000-8000-0026BB765291': 'CarbonMonoxideDetected',
'CarbonMonoxideDetected': '00000069-0000-1000-8000-0026BB765291',
'00000090-0000-1000-8000-0026BB765291': 'CarbonMonoxideLevel',
'CarbonMonoxideLevel': '00000090-0000-1000-8000-0026BB765291',
'00000091-0000-1000-8000-0026BB765291': 'CarbonMonoxidePeakLevel',
'CarbonMonoxidePeakLevel': '00000091-0000-1000-8000-0026BB765291',
'0000008F-0000-1000-8000-0026BB765291': 'ChargingState',
'ChargingState': '0000008F-0000-1000-8000-0026BB765291',
'000000CE-0000-1000-8000-0026BB765291': 'ColorTemperature',
'ColorTemperature': '000000CE-0000-1000-8000-0026BB765291',
'0000006A-0000-1000-8000-0026BB765291': 'ContactSensorState',
'ContactSensorState': '0000006A-0000-1000-8000-0026BB765291',
'0000000D-0000-1000-8000-0026BB765291': 'CoolingThresholdTemperature',
'CoolingThresholdTemperature': '0000000D-0000-1000-8000-0026BB765291',
'000000A9-0000-1000-8000-0026BB765291': 'CurrentAirPurifierState',
'CurrentAirPurifierState': '000000A9-0000-1000-8000-0026BB765291',
'0000006B-0000-1000-8000-0026BB765291': 'CurrentAmbientLightLevel',
'CurrentAmbientLightLevel': '0000006B-0000-1000-8000-0026BB765291',
'0000000E-0000-1000-8000-0026BB765291': 'CurrentDoorState',
'CurrentDoorState': '0000000E-0000-1000-8000-0026BB765291',
'000000AF-0000-1000-8000-0026BB765291': 'CurrentFanState',
'CurrentFanState': '000000AF-0000-1000-8000-0026BB765291',
'000000B1-0000-1000-8000-0026BB765291': 'CurrentHeaterCoolerState',
'CurrentHeaterCoolerState': '000000B1-0000-1000-8000-0026BB765291',
'0000000F-0000-1000-8000-0026BB765291': 'CurrentHeatingCoolingState',
'CurrentHeatingCoolingState': '0000000F-0000-1000-8000-0026BB765291',
'0000006C-0000-1000-8000-0026BB765291': 'CurrentHorizontalTiltAngle',
'CurrentHorizontalTiltAngle': '0000006C-0000-1000-8000-0026BB765291',
'000000B3-0000-1000-8000-0026BB765291': 'CurrentHumidifierDehumidifierState',
'CurrentHumidifierDehumidifierState': '000000B3-0000-1000-8000-0026BB765291',
'0000006D-0000-1000-8000-0026BB765291': 'CurrentPosition',
'CurrentPosition': '0000006D-0000-1000-8000-0026BB765291',
'00000010-0000-1000-8000-0026BB765291': 'CurrentRelativeHumidity',
'CurrentRelativeHumidity': '00000010-0000-1000-8000-0026BB765291',
'000000AA-0000-1000-8000-0026BB765291': 'CurrentSlatState',
'CurrentSlatState': '000000AA-0000-1000-8000-0026BB765291',
'00000011-0000-1000-8000-0026BB765291': 'CurrentTemperature',
'CurrentTemperature': '00000011-0000-1000-8000-0026BB765291',
'000000C1-0000-1000-8000-0026BB765291': 'CurrentTiltAngle',
'CurrentTiltAngle': '000000C1-0000-1000-8000-0026BB765291',
'0000006E-0000-1000-8000-0026BB765291': 'CurrentVerticalTiltAngle',
'CurrentVerticalTiltAngle': '0000006E-0000-1000-8000-0026BB765291',
'0000011D-0000-1000-8000-0026BB765291': 'DigitalZoom',
'DigitalZoom': '0000011D-0000-1000-8000-0026BB765291',
'000000AC-0000-1000-8000-0026BB765291': 'FilterChangeIndication',
'FilterChangeIndication': '000000AC-0000-1000-8000-0026BB765291',
'000000AB-0000-1000-8000-0026BB765291': 'FilterLifeLevel',
'FilterLifeLevel': '000000AB-0000-1000-8000-0026BB765291',
'00000052-0000-1000-8000-0026BB765291': 'FirmwareRevision',
'FirmwareRevision': '00000052-0000-1000-8000-0026BB765291',
'00000053-0000-1000-8000-0026BB765291': 'HardwareRevision',
'HardwareRevision': '00000053-0000-1000-8000-0026BB765291',
'00000012-0000-1000-8000-0026BB765291': 'HeatingThresholdTemperature',
'HeatingThresholdTemperature': '00000012-0000-1000-8000-0026BB765291',
'0000006F-0000-1000-8000-0026BB765291': 'HoldPosition',
'HoldPosition': '0000006F-0000-1000-8000-0026BB765291',
'00000013-0000-1000-8000-0026BB765291': 'Hue',
'Hue': '00000013-0000-1000-8000-0026BB765291',
'00000014-0000-1000-8000-0026BB765291': 'Identify',
'Identify': '00000014-0000-1000-8000-0026BB765291',
'0000011F-0000-1000-8000-0026BB765291': 'ImageMirroring',
'ImageMirroring': '0000011F-0000-1000-8000-0026BB765291',
'0000011E-0000-1000-8000-0026BB765291': 'ImageRotation',
'ImageRotation': '0000011E-0000-1000-8000-0026BB765291',
'000000D2-0000-1000-8000-0026BB765291': 'InUse',
'InUse': '000000D2-0000-1000-8000-0026BB765291',
'000000D6-0000-1000-8000-0026BB765291': 'IsConfigured',
'IsConfigured': '000000D6-0000-1000-8000-0026BB765291',
'00000070-0000-1000-8000-0026BB765291': 'LeakDetected',
'LeakDetected': '00000070-0000-1000-8000-0026BB765291',
'00000019-0000-1000-8000-0026BB765291': 'LockControlPoint',
'LockControlPoint': '00000019-0000-1000-8000-0026BB765291',
'0000001D-0000-1000-8000-0026BB765291': 'LockCurrentState',
'LockCurrentState': '0000001D-0000-1000-8000-0026BB765291',
'0000001C-0000-1000-8000-0026BB765291': 'LockLastKnownAction',
'LockLastKnownAction': '0000001C-0000-1000-8000-0026BB765291',
'0000001A-0000-1000-8000-0026BB765291': 'LockManagementAutoSecurityTimeout',
'LockManagementAutoSecurityTimeout': '0000001A-0000-1000-8000-0026BB765291',
'000000A7-0000-1000-8000-0026BB765291': 'LockPhysicalControls',
'LockPhysicalControls': '000000A7-0000-1000-8000-0026BB765291',
'0000001E-0000-1000-8000-0026BB765291': 'LockTargetState',
'LockTargetState': '0000001E-0000-1000-8000-0026BB765291',
'0000001F-0000-1000-8000-0026BB765291': 'Logs',
'Logs': '0000001F-0000-1000-8000-0026BB765291',
'00000020-0000-1000-8000-0026BB765291': 'Manufacturer',
'Manufacturer': '00000020-0000-1000-8000-0026BB765291',
'00000021-0000-1000-8000-0026BB765291': 'Model',
'Model': '00000021-0000-1000-8000-0026BB765291',
'00000022-0000-1000-8000-0026BB765291': 'MotionDetected',
'MotionDetected': '00000022-0000-1000-8000-0026BB765291',
'0000011A-0000-1000-8000-0026BB765291': 'Mute',
'Mute': '0000011A-0000-1000-8000-0026BB765291',
'00000023-0000-1000-8000-0026BB765291': 'Name',
'Name': '00000023-0000-1000-8000-0026BB765291',
'0000011B-0000-1000-8000-0026BB765291': 'NightVision',
'NightVision': '0000011B-0000-1000-8000-0026BB765291',
'000000C4-0000-1000-8000-0026BB765291': 'NitrogenDioxideDensity',
'NitrogenDioxideDensity': '000000C4-0000-1000-8000-0026BB765291',
'00000024-0000-1000-8000-0026BB765291': 'ObstructionDetected',
'ObstructionDetected': '00000024-0000-1000-8000-0026BB765291',
'00000071-0000-1000-8000-0026BB765291': 'OccupancyDetected',
'OccupancyDetected': '00000071-0000-1000-8000-0026BB765291',
'00000025-0000-1000-8000-0026BB765291': 'On',
'On': '00000025-0000-1000-8000-0026BB765291',
'0000011C-0000-1000-8000-0026BB765291': 'OpticalZoom',
'OpticalZoom': '0000011C-0000-1000-8000-0026BB765291',
'00000026-0000-1000-8000-0026BB765291': 'OutletInUse',
'OutletInUse': '00000026-0000-1000-8000-0026BB765291',
'000000C3-0000-1000-8000-0026BB765291': 'OzoneDensity',
'OzoneDensity': '000000C3-0000-1000-8000-0026BB765291',
'0000004C-0000-1000-8000-0026BB765291': 'PairSetup',
'PairSetup': '0000004C-0000-1000-8000-0026BB765291',
'0000004E-0000-1000-8000-0026BB765291': 'PairVerify',
'PairVerify': '0000004E-0000-1000-8000-0026BB765291',
'0000004F-0000-1000-8000-0026BB765291': 'PairingFeatures',
'PairingFeatures': '0000004F-0000-1000-8000-0026BB765291',
'00000050-0000-1000-8000-0026BB765291': 'PairingPairings',
'PairingPairings': '00000050-0000-1000-8000-0026BB765291',
'000000C7-0000-1000-8000-0026BB765291': 'PM10Density',
'PM10Density': '000000C7-0000-1000-8000-0026BB765291',
'000000C6-0000-1000-8000-0026BB765291': 'PM2_5Density',
'PM2_5Density': '000000C6-0000-1000-8000-0026BB765291',
'00000072-0000-1000-8000-0026BB765291': 'PositionState',
'PositionState': '00000072-0000-1000-8000-0026BB765291',
'000000D1-0000-1000-8000-0026BB765291': 'ProgramMode',
'ProgramMode': '000000D1-0000-1000-8000-0026BB765291',
'00000073-0000-1000-8000-0026BB765291': 'ProgrammableSwitchEvent',
'ProgrammableSwitchEvent': '00000073-0000-1000-8000-0026BB765291',
'000000C9-0000-1000-8000-0026BB765291': 'RelativeHumidityDehumidifierThreshold',
'RelativeHumidityDehumidifierThreshold': '000000C9-0000-1000-8000-0026BB765291',
'000000CA-0000-1000-8000-0026BB765291': 'RelativeHumidityHumidifierThreshold',
'RelativeHumidityHumidifierThreshold': '000000CA-0000-1000-8000-0026BB765291',
'000000D4-0000-1000-8000-0026BB765291': 'RemainingDuration',
'RemainingDuration': '000000D4-0000-1000-8000-0026BB765291',
'000000AD-0000-1000-8000-0026BB765291': 'ResetFilterIndication',
'ResetFilterIndication': '000000AD-0000-1000-8000-0026BB765291',
'00000028-0000-1000-8000-0026BB765291': 'RotationDirection',
'RotationDirection': '00000028-0000-1000-8000-0026BB765291',
'00000029-0000-1000-8000-0026BB765291': 'RotationSpeed',
'RotationSpeed': '00000029-0000-1000-8000-0026BB765291',
'0000002F-0000-1000-8000-0026BB765291': 'Saturation',
'Saturation': '0000002F-0000-1000-8000-0026BB765291',
'0000008E-0000-1000-8000-0026BB765291': 'SecuritySystemAlarmType',
'SecuritySystemAlarmType': '0000008E-0000-1000-8000-0026BB765291',
'00000066-0000-1000-8000-0026BB765291': 'SecuritySystemCurrentState',
'SecuritySystemCurrentState': '00000066-0000-1000-8000-0026BB765291',
'00000067-0000-1000-8000-0026BB765291': 'SecuritySystemTargetState',
'SecuritySystemTargetState': '00000067-0000-1000-8000-0026BB765291',
'00000117-0000-1000-8000-0026BB765291': 'SelectedRTPStreamConfiguration',
'SelectedRTPStreamConfiguration': '00000117-0000-1000-8000-0026BB765291',
'00000030-0000-1000-8000-0026BB765291': 'SerialNumber',
'SerialNumber': '00000030-0000-1000-8000-0026BB765291',
'000000CB-0000-1000-8000-0026BB765291': 'ServiceLabelIndex',
'ServiceLabelIndex': '000000CB-0000-1000-8000-0026BB765291',
'000000CD-0000-1000-8000-0026BB765291': 'ServiceLabelNamespace',
'ServiceLabelNamespace': '000000CD-0000-1000-8000-0026BB765291',
'000000D3-0000-1000-8000-0026BB765291': 'SetDuration',
'SetDuration': '000000D3-0000-1000-8000-0026BB765291',
'00000118-0000-1000-8000-0026BB765291': 'SetupEndpoints',
'SetupEndpoints': '00000118-0000-1000-8000-0026BB765291',
'000000C0-0000-1000-8000-0026BB765291': 'SlatType',
'SlatType': '000000C0-0000-1000-8000-0026BB765291',
'00000076-0000-1000-8000-0026BB765291': 'SmokeDetected',
'SmokeDetected': '00000076-0000-1000-8000-0026BB765291',
'00000075-0000-1000-8000-0026BB765291': 'StatusActive',
'StatusActive': '00000075-0000-1000-8000-0026BB765291',
'00000077-0000-1000-8000-0026BB765291': 'StatusFault',
'StatusFault': '00000077-0000-1000-8000-0026BB765291',
'00000078-0000-1000-8000-0026BB765291': 'StatusJammed',
'StatusJammed': '00000078-0000-1000-8000-0026BB765291',
'00000079-0000-1000-8000-0026BB765291': 'StatusLowBattery',
'StatusLowBattery': '00000079-0000-1000-8000-0026BB765291',
'0000007A-0000-1000-8000-0026BB765291': 'StatusTampered',
'StatusTampered': '0000007A-0000-1000-8000-0026BB765291',
'00000120-0000-1000-8000-0026BB765291': 'StreamingStatus',
'StreamingStatus': '00000120-0000-1000-8000-0026BB765291',
'000000C5-0000-1000-8000-0026BB765291': 'SulphurDioxideDensity',
'SulphurDioxideDensity': '000000C5-0000-1000-8000-0026BB765291',
'00000115-0000-1000-8000-0026BB765291': 'SupportedAudioStreamConfiguration',
'SupportedAudioStreamConfiguration': '00000115-0000-1000-8000-0026BB765291',
'00000116-0000-1000-8000-0026BB765291': 'SupportedRTPConfiguration',
'SupportedRTPConfiguration': '00000116-0000-1000-8000-0026BB765291',
'00000114-0000-1000-8000-0026BB765291': 'SupportedVideoStreamConfiguration',
'SupportedVideoStreamConfiguration': '00000114-0000-1000-8000-0026BB765291',
'000000B6-0000-1000-8000-0026BB765291': 'SwingMode',
'SwingMode': '000000B6-0000-1000-8000-0026BB765291',
'000000A8-0000-1000-8000-0026BB765291': 'TargetAirPurifierState',
'TargetAirPurifierState': '000000A8-0000-1000-8000-0026BB765291',
'000000AE-0000-1000-8000-0026BB765291': 'TargetAirQuality',
'TargetAirQuality': '000000AE-0000-1000-8000-0026BB765291',
'00000032-0000-1000-8000-0026BB765291': 'TargetDoorState',
'TargetDoorState': '00000032-0000-1000-8000-0026BB765291',
'000000BF-0000-1000-8000-0026BB765291': 'TargetFanState',
'TargetFanState': '000000BF-0000-1000-8000-0026BB765291',
'000000B2-0000-1000-8000-0026BB765291': 'TargetHeaterCoolerState',
'TargetHeaterCoolerState': '000000B2-0000-1000-8000-0026BB765291',
'00000033-0000-1000-8000-0026BB765291': 'TargetHeatingCoolingState',
'TargetHeatingCoolingState': '00000033-0000-1000-8000-0026BB765291',
'0000007B-0000-1000-8000-0026BB765291': 'TargetHorizontalTiltAngle',
'TargetHorizontalTiltAngle': '0000007B-0000-1000-8000-0026BB765291',
'000000B4-0000-1000-8000-0026BB765291': 'TargetHumidifierDehumidifierState',
'TargetHumidifierDehumidifierState': '000000B4-0000-1000-8000-0026BB765291',
'0000007C-0000-1000-8000-0026BB765291': 'TargetPosition',
'TargetPosition': '0000007C-0000-1000-8000-0026BB765291',
'00000034-0000-1000-8000-0026BB765291': 'TargetRelativeHumidity',
'TargetRelativeHumidity': '00000034-0000-1000-8000-0026BB765291',
'000000BE-0000-1000-8000-0026BB765291': 'TargetSlatState',
'TargetSlatState': '000000BE-0000-1000-8000-0026BB765291',
'00000035-0000-1000-8000-0026BB765291': 'TargetTemperature',
'TargetTemperature': '00000035-0000-1000-8000-0026BB765291',
'000000C2-0000-1000-8000-0026BB765291': 'TargetTiltAngle',
'TargetTiltAngle': '000000C2-0000-1000-8000-0026BB765291',
'0000007D-0000-1000-8000-0026BB765291': 'TargetVerticalTiltAngle',
'TargetVerticalTiltAngle': '0000007D-0000-1000-8000-0026BB765291',
'00000036-0000-1000-8000-0026BB765291': 'TemperatureDisplayUnits',
'TemperatureDisplayUnits': '00000036-0000-1000-8000-0026BB765291',
'000000D5-0000-1000-8000-0026BB765291': 'ValveType',
'ValveType': '000000D5-0000-1000-8000-0026BB765291',
'00000037-0000-1000-8000-0026BB765291': 'Version',
'Version': '00000037-0000-1000-8000-0026BB765291',
'000000C8-0000-1000-8000-0026BB765291': 'VOCDensity',
'VOCDensity': '000000C8-0000-1000-8000-0026BB765291',
'00000119-0000-1000-8000-0026BB765291': 'Volume',
'Volume': '00000119-0000-1000-8000-0026BB765291',
'000000B5-0000-1000-8000-0026BB765291': 'WaterLevel',
'WaterLevel': '000000B5-0000-1000-8000-0026BB765291',
'000000E7-0000-1000-8000-0026BB765291': 'ActiveIdentifier',
'ActiveIdentifier': '000000E7-0000-1000-8000-0026BB765291',
'000000E3-0000-1000-8000-0026BB765291': 'ConfiguredName',
'ConfiguredName': '000000E3-0000-1000-8000-0026BB765291',
'000000E8-0000-1000-8000-0026BB765291': 'SleepDiscoveryMode',
'SleepDiscoveryMode': '000000E8-0000-1000-8000-0026BB765291',
'000000DD-0000-1000-8000-0026BB765291': 'ClosedCaptions',
'ClosedCaptions': '000000DD-0000-1000-8000-0026BB765291',
'00000136-0000-1000-8000-0026BB765291': 'DisplayOrder',
'DisplayOrder': '00000136-0000-1000-8000-0026BB765291',
'000000E0-0000-1000-8000-0026BB765291': 'CurrentMediaState',
'CurrentMediaState': '000000E0-0000-1000-8000-0026BB765291',
'00000137-0000-1000-8000-0026BB765291': 'TargetMediaState',
'TargetMediaState': '00000137-0000-1000-8000-0026BB765291',
'000000E2-0000-1000-8000-0026BB765291': 'PictureMode',
'PictureMode': '000000E2-0000-1000-8000-0026BB765291',
'000000DF-0000-1000-8000-0026BB765291': 'PowerModeSelection',
'PowerModeSelection': '000000DF-0000-1000-8000-0026BB765291',
'000000E1-0000-1000-8000-0026BB765291': 'RemoteKey',
'RemoteKey': '000000E1-0000-1000-8000-0026BB765291',
'000000DB-0000-1000-8000-0026BB765291': 'InputSourceType',
'InputSourceType': '000000DB-0000-1000-8000-0026BB765291',
'000000DC-0000-1000-8000-0026BB765291': 'InputDeviceType',
'InputDeviceType': '000000DC-0000-1000-8000-0026BB765291',
'000000E6-0000-1000-8000-0026BB765291': 'Identifier',
'Identifier': '000000E6-0000-1000-8000-0026BB765291',
'00000135-0000-1000-8000-0026BB765291': 'CurrentVisibilityState',
'CurrentVisibilityState': '00000135-0000-1000-8000-0026BB765291',
'00000134-0000-1000-8000-0026BB765291': 'TargetVisibilityState',
'TargetVisibilityState': '00000134-0000-1000-8000-0026BB765291',
'000000E9-0000-1000-8000-0026BB765291': 'VolumeControlType',
'VolumeControlType': '000000E9-0000-1000-8000-0026BB765291',
'000000EA-0000-1000-8000-0026BB765291': 'VolumeSelector',
'VolumeSelector': '000000EA-0000-1000-8000-0026BB765291',
};

425
src/3rdParty/HapClient/index.ts vendored Executable file
View File

@ -0,0 +1,425 @@
import 'source-map-support/register';
import * as crypto from 'crypto';
import decamelize from 'decamelize';
import * as inflection from 'inflection';
import Bonjour, { Service } from 'bonjour';
import { EventEmitter } from 'events';
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';
export type HapAccessoriesRespType = IHapAccessoriesRespType;
export type ServiceType = IServiceType;
export type CharacteristicType = ICharacteristicType;
export interface IDevice {
addresses: Array<string>
fqdn: string,
host: string,
name: string,
port: number,
protocol: "tcp" | "udp",
rawTxt: Buffer,
referer: {
address: string,
family: "IPv4" | "IPv6",
port: number,
size: number,
},
subtypes: Array<any>,
txt: any,
type: string
}
export class HapClient extends EventEmitter {
private bonjour = Bonjour();
private browser?: Bonjour.Browser;
private discoveryInProgress = false;
private log: (msg: string) => void;
private pin: string;
private debugEnabled: boolean;
private config: {
debug?: boolean;
instanceBlacklist?: string[];
};
private instances: IHapInstance[] = [];
private hiddenServices = [
Services.AccessoryInformation
];
private hiddenCharacteristics = [
Characteristics.Name
];
constructor(opts: {
pin: string;
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) {
if (this.debugEnabled) {
this.log(msg);
}
}
private debugErr(msg: string, err: Error) {
if (this.debugEnabled) {
this.log(`${msg}: ${err.message}\n ${err.stack}`);
}
}
public refreshInstances() {
if (!this.discoveryInProgress) {
this.startDiscovery();
} else {
try {
this.debug(`[HapClient] Discovery :: Re-broadcasting discovery query`);
if (this.browser) {
this.browser.update();
}
} catch (e) { }
}
}
private async startDiscovery() {
this.discoveryInProgress = true;
this.browser = this.bonjour.find({
type: 'hap'
});
// 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);
}
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,
});
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`);
}
}
}
// 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}`);
}
});
}
private async getAccessories() {
if (!this.instances.length) {
this.debug('[HapClient] Cannot load accessories. No Homebridge instances have been discovered.');
}
const accessories = [];
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) {
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);
}
}
}
return accessories;
}
public async monitorCharacteristics(): Promise<HapMonitor> {
const services = await this.getAllServices();
return new HapMonitor(this.log, this.debug.bind(this), this.pin, services);
}
public async getAllServices(): Promise<Array<IServiceType>> {
/* Get Accessories from HAP */
const accessories = await this.getAccessories();
const services: Array<IServiceType> = [];
/* Parse All Accessories */
accessories.forEach(accessory => {
/* Parse Accessory Information */
const accessoryInformationService = accessory.services.find(x => x.type === Services.AccessoryInformation);
const accessoryInformation: { [key: string]: any } = {};
if (accessoryInformationService && accessoryInformationService.characteristics) {
accessoryInformationService.characteristics.forEach((c) => {
if (c.value) {
accessoryInformation[c.description] = c.value;
}
});
}
/* Parse All Services */
accessory.services
.filter((s) => this.hiddenServices.indexOf(s.type) < 0 && Services[s.type])
.map((s) => {
let serviceName = s.characteristics.find(x => x.type === Characteristics.Name);
/* Set default name characteristic if none defined */
serviceName = serviceName ? serviceName : {
iid: 0,
type: Characteristics.Name,
description: 'Name',
format: 'string',
value: this.humanizeString(Services[s.type]),
perms: ['pr']
};
/* Parse Service Characteristics */
const serviceCharacteristics: Array<ICharacteristicType> = s.characteristics
.filter((c) => this.hiddenCharacteristics.indexOf(c.type) < 0 && Characteristics[c.type])
.map((c) => {
return {
aid: accessory.aid,
iid: c.iid,
uuid: c.type,
type: Characteristics[c.type],
serviceType: Services[s.type],
serviceName: serviceName!.value.toString(),
description: c.description,
value: c.value,
format: c.format,
perms: c.perms,
unit: c.unit,
maxValue: c.maxValue,
minValue: c.minValue,
minStep: c.minStep,
canRead: c.perms.includes('pr'),
canWrite: c.perms.includes('pw'),
ev: c.perms.includes('ev'),
};
});
const service: IServiceType = {
aid: accessory.aid,
iid: s.iid,
uuid: s.type,
type: Services[s.type],
humanType: this.humanizeString(Services[s.type]),
serviceName: serviceName.value.toString(),
serviceCharacteristics: serviceCharacteristics,
accessoryInformation: accessoryInformation,
values: {},
linked: s.linked,
instance: accessory.instance,
};
// generate unique id for service
service.uniqueId = crypto.createHash('sha256')
.update(`${service.instance.username}${service.aid}${service.iid}${service.type}`)
.digest('hex');
/* Helper function to trigger a call to the accessory to get all the characteristic values */
service.refreshCharacteristics = () => {
return this.refreshServiceCharacteristics.bind(this)(service);
};
/* Helper function to set the value of a characteristic */
service.setCharacteristic = (iid: number, value: number | string | boolean): Promise<IServiceType | ICharacteristicType> => {
return this.setCharacteristic.bind(this)(service, iid, value);
};
/* Helper function to returns a characteristic by it's type name */
service.getCharacteristic = (type: string): ICharacteristicType => {
let characteristic = service.serviceCharacteristics.find(c => c.type === type);
return characteristic ? characteristic : createDefaultCharacteristicType();
};
service.serviceCharacteristics.forEach((cType: ICharacteristicType) => {
/* Helper function to set the value of a characteristic */
cType.setValue = async (value: number | string | boolean): Promise<ICharacteristicType> => {
return await this.setCharacteristic.bind(this)(service, cType.iid, value);
};
/* Helper function to get the value of a characteristic from the accessory */
cType.getValue = async (): Promise<ICharacteristicType> => {
return await this.getCharacteristic.bind(this)(service, cType.iid);
};
/* set the values for each characteristic type in an easy-to-access object */
service.values[cType.type] = cType.value;
});
services.push(service);
});
});
return services;
}
async getService(iid: number) {
const services = await this.getAllServices();
return services.find(x => x.iid === iid);
}
async getServiceByName(serviceName: string) {
const services = await this.getAllServices();
return services.find(x => x.serviceName === serviceName);
}
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`, {
qs: {
id: iids.map(iid => `${service.aid}.${iid}`).join(',')
},
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;
});
return service;
}
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}`
},
json: true
});
const characteristic = service.serviceCharacteristics.find(x => x.iid === resp.characteristics[0].iid && x.aid === service.aid);
if (characteristic) {
characteristic.value = resp.characteristics[0].value;
}
return characteristic ? characteristic : createDefaultCharacteristicType();
}
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`, {
headers: {
Authorization: this.pin
},
json: {
characteristics: [
{
aid: service.aid,
iid: iid,
value: value
}
]
}
});
characteristic = await this.getCharacteristic(service, iid);
} catch (e) {
if (this.log) {
this.debugErr(`[HapClient] [${service.instance.ipAddress}:${service.instance.port} (${service.instance.username})] ` +
`Failed to set value for ${service.serviceName}.`, e);
if (e.statusCode === 401) {
this.debug(`[HapClient] [${service.instance.ipAddress}:${service.instance.port} (${service.instance.username})] ` +
`Make sure Homebridge pin for this instance is set to ${this.pin}.`);
} else {
this.debug(e.message);
}
} else {
console.log(e);
}
}
return characteristic
}
private humanizeString(string: string) {
return inflection.titleize(decamelize(string));
}
}

116
src/3rdParty/HapClient/interfaces.ts vendored Executable file
View File

@ -0,0 +1,116 @@
import { Socket } from 'net';
export interface IHapInstance {
displayName: string
name: string;
ipAddress: string;
port: number;
username: string;
}
export interface IHapEvInstance {
name: string;
ipAddress: string;
port: number;
username: string;
evCharacteristics?: { aid: number, iid: number, ev: boolean }[];
socket?: Socket;
}
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;
}>;
}>;
}>;
}
export interface IServiceType {
aid: number;
iid: number;
uuid: string;
type: string;
linked?: Array<number>;
linkedServices?: {
[iid: number]: IServiceType;
};
hidden?: boolean;
humanType: string;
serviceName: string;
serviceCharacteristics: ICharacteristicType[];
accessoryInformation: any;
refreshCharacteristics?: () => Promise<IServiceType>;
setCharacteristic?: (iid: number, value: number | string | boolean) => Promise<IServiceType | ICharacteristicType>;
getCharacteristic?: (type: string) => ICharacteristicType;
values: any;
instance: {
ipAddress: string;
port: number;
username: string;
name: string;
};
uniqueId?: string;
}
export interface ICharacteristicType {
aid: number;
iid: number;
uuid: string;
type: string;
serviceType: string;
serviceName: 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;
canRead: boolean;
canWrite: boolean;
ev: boolean;
setValue?: (value: number | string | boolean) => Promise<ICharacteristicType>;
getValue?: () => Promise<ICharacteristicType>;
}
export const createDefaultCharacteristicType = (): ICharacteristicType => {
return {
aid: -1,
iid: -1,
uuid: "",
type: "",
serviceType: "",
serviceName: "",
description: "",
value: "",
format: 'bool',
perms: [],
canRead: false,
canWrite: false,
ev: false,
};
}

117
src/3rdParty/HapClient/monitor.ts vendored Executable file
View File

@ -0,0 +1,117 @@
import { EventEmitter } from 'events';
import { IServiceType, IHapEvInstance } from './interfaces';
import { createConnection, parseMessage } from './eventedHttpClient';
import { CharacteristicType } from '.';
export class HapMonitor extends EventEmitter {
private pin: string;
private evInstances: IHapEvInstance[];
private services: IServiceType[];
private logger: any;
private debug: any;
constructor(logger: any, debug: any, pin: string, services: IServiceType[]) {
super();
this.logger = logger;
this.debug = debug;
this.pin = pin;
this.services = services;
this.evInstances = [];
// get a list of characteristics we can watch for each instance
this.parseServices();
// start watching
this.start();
}
start() {
for (const instance of this.evInstances) {
instance.socket = createConnection(instance, this.pin, { characteristics: instance.evCharacteristics });
this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] Connected`);
instance.socket.on('data', (data) => {
const message = parseMessage(data);
if (message.statusCode === 401) {
if (this.logger) {
this.logger.warn(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] ` +
`${message.statusCode} ${message.statusMessage} - make sure Homebridge pin for this instance is set to ${this.pin}.`);
}
}
if (message.protocol === 'EVENT') {
try {
const body = JSON.parse(message.body);
if (body.characteristics && body.characteristics.length) {
this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] ` +
`Got Event: ${JSON.stringify(body.characteristics)}`);
const response = body.characteristics.map((c: CharacteristicType): IServiceType | undefined => {
// find the matching service for each characteristics
const service = this.services.find(x => x.aid === c.aid && x.instance.username === instance.username);
if (service) {
// find the correct characteristic and update it
const characteristic = service.serviceCharacteristics.find(x => x.iid === c.iid);
if (characteristic) {
characteristic.value = c.value;
service.values[characteristic.type] = c.value;
return service;
}
}
return undefined;
});
// push update to listeners
this.emit('service-update', response.filter((x: any) => x));
}
} catch (e) {
// do nothing
}
}
});
}
}
finish() {
for (const instance of this.evInstances) {
if (instance.socket) {
try {
instance.socket.destroy();
this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] Disconnected`);
} catch (e) {
// do nothing
}
}
}
}
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'));
if (evCharacteristics.length) {
// register the instance if it's not already there
if (!this.evInstances.find(x => x.username === service.instance.username)) {
const newInstance = Object.assign({}, service.instance) as IHapEvInstance;
newInstance.evCharacteristics = [];
this.evInstances.push(newInstance);
}
const instance = this.evInstances.find(x => x.username === service.instance.username);
for (const evCharacteristic of evCharacteristics) {
if (!instance!.evCharacteristics!.find(x => x.aid === service.aid && x.iid === evCharacteristic.iid)) {
instance!.evCharacteristics!.push({ aid: service.aid, iid: evCharacteristic.iid, ev: true });
}
}
}
}
}
}

1
src/Types/types.ts Normal file
View File

@ -0,0 +1 @@
export type Dictionary<T> = { [key: string]: T };

View File

@ -1,3 +1,6 @@
import { HapClient } from "./3rdParty/HapClient";
import { HapMonitor } from "./3rdParty/HapClient/monitor";
let Accessory: any;
let Homebridge: any;
@ -20,6 +23,7 @@ class AutomationPlatform {
log: any = {};
config: any = {};
api: any;
client: HapClient;
constructor(log: any, config: any, api: any) {
this.log = log;
@ -28,8 +32,16 @@ class AutomationPlatform {
this.log('INFO - Registering automation platform');
this.api.on('didFinishLaunching', this.didFinishLaunching.bind(this));
// let asdf: any = api.getAllServices();
// let asdff = "asdf";
this.client = new HapClient({
pin: "031-45-154",
logger: log,
config: config
});
this.client.on('instance-discovered', async (instance: any) => {
let asdf = instance;
})
}