UNPKG

hap-controller

Version:

Library to implement a HAP (HomeKit) controller

940 lines (939 loc) 74.1 kB
"use strict"; /** * Controller class for interacting with a HAP device over GATT. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const Characteristic = __importStar(require("../../model/characteristic")); const events_1 = require("events"); const gatt_connection_1 = __importDefault(require("./gatt-connection")); const GattConstants = __importStar(require("./gatt-constants")); const gatt_protocol_1 = __importDefault(require("./gatt-protocol")); const GattUtils = __importStar(require("./gatt-utils")); const pairing_protocol_1 = __importStar(require("../../protocol/pairing-protocol")); const Service = __importStar(require("../../model/service")); const tlv_1 = require("../../model/tlv"); const ip_discovery_1 = require("../ip/ip-discovery"); const debug_1 = __importDefault(require("debug")); const queue_1 = require("../../utils/queue"); const debug = (0, debug_1.default)('hap-controller:gatt-client'); class GattClient extends events_1.EventEmitter { /** * Initialize the GattClient object. * * @param {string} deviceId - ID of the device * @param {NoblePeripheral} peripheral - Peripheral object from noble * @param {PairingData?} pairingData - existing pairing data */ constructor(deviceId, peripheral, pairingData) { super(); this.subscribedCharacteristics = []; this.deviceId = deviceId; this.peripheral = peripheral; this.pairingProtocol = new pairing_protocol_1.default(pairingData); this.gattProtocol = new gatt_protocol_1.default(); this.tid = Math.floor(Math.random() * 254); this.queue = new queue_1.OpQueue(); this.pairingQueue = new queue_1.OpQueue(); } /** * Queue an operation for the client. * * @param {function} op - Function to add to the queue * @returns {Promise} Promise which resolves when the function is called. */ _queueOperation(op) { return this.queue.queue(op); } /** * Queue an operation for the pairing. * * @param {function} op - Function to add to the queue * @returns {Promise} Promise which resolves when the function is called. */ _queuePairingOperation(op) { return this.pairingQueue.queue(op); } /** * Get the next transaction ID. * * @returns {number} Transaction ID. */ getNextTransactionId() { this.tid++; if (this.tid > 255) { this.tid = 0; } return this.tid; } /** * Get the data (keys) that needs to be stored long-term. * * @returns {PairingData} Object containing the keys that should be stored. */ getLongTermData() { return this.pairingProtocol.getLongTermData(); } /** * Verify the provided PIN * * @param pin {string} PIN */ verifyPin(pin) { this.pairingProtocol.verifyPin(pin); } /** * Run the identify routine on a device. * * @returns {Promise} Promise which resolves if identify succeeded. */ identify() { const serviceUuid = GattUtils.uuidToNobleUuid(Service.uuidFromService('public.hap.service.accessory-information')); const characteristicUuid = GattUtils.uuidToNobleUuid(Characteristic.uuidFromCharacteristic('public.hap.characteristic.identify')); return this._queueOperation(async () => { const connection = new gatt_connection_1.default(this.peripheral); try { await connection.connect(); const { characteristics } = await new GattUtils.Watcher(this.peripheral, this.peripheral.discoverSomeServicesAndCharacteristicsAsync([serviceUuid], [characteristicUuid])).getPromise(); const characteristic = characteristics.find((c) => { return c.uuid === characteristicUuid; }); if (!characteristic) { throw new Error('Identify characteristic not found'); } const iid = await this._readInstanceId(characteristic); const data = new Map(); data.set(GattConstants.Types['HAP-Param-Value'], Buffer.from([1])); const pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, data); const pdus = await connection.writeCharacteristic(characteristic, [pdu]); if (pdus.length === 0) { throw new Error('No response from identify routine'); } const status = pdus[0].readUInt8(2); if (status !== 0) { throw new Error(`Identify returned error status: ${status}`); } // eslint-disable-next-line @typescript-eslint/no-empty-function await connection.disconnect().catch(() => { }); } catch (err) { // eslint-disable-next-line @typescript-eslint/no-empty-function await connection.disconnect().catch(() => { }); throw err; } }); } /** * Read the instance ID descriptor for a characteristic. The peripheral must * already be connected. * * @param {Object} characteristic - The characteristic to read from * @returns {Promise} Promise which resolves to the IID. */ async _readInstanceId(characteristic) { const characteristicInstanceIdUuid = GattUtils.uuidToNobleUuid(GattConstants.CharacteristicInstanceIdUuid); const characteristicInstanceIdShortUuid = GattUtils.uuidToNobleUuid(GattConstants.CharacteristicInstanceIdShortUuid); const descriptors = await new GattUtils.Watcher(this.peripheral, characteristic.discoverDescriptorsAsync()).getPromise(); const descriptor = descriptors.find((d) => { return d.uuid === characteristicInstanceIdUuid || d.uuid === characteristicInstanceIdShortUuid; }); if (!descriptor) { throw new Error('Could not find IID'); } const data = await new GattUtils.Watcher(this.peripheral, descriptor.readValueAsync()).getPromise(); return data.readUInt16LE(0); } async getPairingMethod() { const serviceUuid = GattUtils.uuidToNobleUuid(Service.uuidFromService('public.hap.service.pairing')); const featureCharacteristicUuid = GattUtils.uuidToNobleUuid(Characteristic.uuidFromCharacteristic('public.hap.characteristic.pairing.features')); return this._queueOperation(async () => { const connection = new gatt_connection_1.default(this.peripheral); try { await connection.connect(); const { characteristics } = await new GattUtils.Watcher(this.peripheral, this.peripheral.discoverSomeServicesAndCharacteristicsAsync([serviceUuid], [featureCharacteristicUuid])).getPromise(); const characteristic = characteristics.find((c) => { return c.uuid === featureCharacteristicUuid; }); if (!characteristic) { throw new Error('pairing.features characteristic not found'); } const iid = await this._readInstanceId(characteristic); const pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid); const pdus = await connection.writeCharacteristic(characteristic, [pdu]); // eslint-disable-next-line @typescript-eslint/no-empty-function await connection.disconnect().catch(() => { }); if (pdus.length !== 0) { const response = pdus[0]; const pairFeatures = response.readUInt8(); const pairMethod = pairFeatures & ip_discovery_1.DiscoveryPairingFeatureFlags.SupportsAppleAuthenticationCoprocessor ? pairing_protocol_1.PairMethods.PairSetupWithAuth : pairing_protocol_1.PairMethods.PairSetup; return pairMethod; } else { throw new Error('Could not read the Pairing Feature information'); } } catch (err) { // eslint-disable-next-line @typescript-eslint/no-empty-function await connection.disconnect().catch(() => { }); throw err; } }); } /** * Begins the pairing process. For devices with random pins, this * will cause it to show the pin on the screen. * * @param {PairMethods} [pairMethod] - Method to use for pairing, default is PairSetupWithAuth * @param {PairingTypeFlags} [pairFlags] - Flags to use for Pairing for PairSetup * * No provided flags is equivalent to providing * kPairingFlag_Transient and kPairingFlag_Split and in this case a new * code is generated randomly by the device (if supported) or the * pre-defined code is used * * If only the flag kPairingFlag_Split is provided the code which * was created on the device from last transient+split call is reused * and needs to be provided in finishPairing by user of this library * * If only the flag kPairingFlag_Transient is provided the session * security is enabled but no final pairing is done * @returns {Promise} Promise which resolves to opaque * pairing data when complete. */ async startPairing(pairMethod = pairing_protocol_1.PairMethods.PairSetupWithAuth, pairFlags = 0) { const serviceUuid = GattUtils.uuidToNobleUuid(Service.uuidFromService('public.hap.service.pairing')); const characteristicUuid = GattUtils.uuidToNobleUuid(Characteristic.uuidFromCharacteristic('public.hap.characteristic.pairing.pair-setup')); return this._queueOperation(async () => { const connection = (this._pairingConnection = new gatt_connection_1.default(this.peripheral)); await connection.connect(); const { characteristics } = await new GattUtils.Watcher(this.peripheral, this.peripheral.discoverSomeServicesAndCharacteristicsAsync([serviceUuid], [characteristicUuid])).getPromise(); const characteristic = characteristics.find((c) => { return c.uuid === characteristicUuid; }); if (!characteristic) { throw new Error('pair-setup characteristic not found'); } const iid = await this._readInstanceId(characteristic); const packet = await this.pairingProtocol.buildPairSetupM1(pairMethod, pairFlags); const data = new Map(); data.set(GattConstants.Types['HAP-Param-Value'], packet); data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1])); let pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, data); let pdus = await connection.writeCharacteristic(characteristic, [pdu]); if (pdus.length === 0) { throw new Error('M1: No response'); } let response = pdus[0]; let status = response.readUInt8(2); if (status !== 0) { throw new Error(`M1: Got error status: ${status}`); } if (response.length < 5) { pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid); pdus = await connection.writeCharacteristic(characteristic, [pdu]); if (pdus.length === 0) { throw new Error('M2: No response'); } response = pdus[0]; status = response.readUInt8(2); if (status !== 0) { throw new Error(`M2: Got error status: ${status}`); } } const body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length)); if (!body.has(GattConstants.Types['HAP-Param-Value'])) { throw new Error('M2: HAP-Param-Value missing'); } const tlv = await this.pairingProtocol.parsePairSetupM2(body.get(GattConstants.Types['HAP-Param-Value'])); return { tlv, iid, characteristic }; }); } /** * Finishes a pairing process that began with startPairing() * * @param {PairingData} pairingData - The pairing data returned from startPairing() * @param {string} pin - The pairing PIN, needs to be formatted as XXX-XX-XXX * @returns {Promise} Promise which resolves when pairing is complete. */ async finishPairing(pairingData, pin) { const { tlv, iid, characteristic } = pairingData; this.verifyPin(pin); if (!this._pairingConnection) { throw new Error('Must call startPairing() first'); } return this._queueOperation(async () => { const connection = this._pairingConnection; const protocol = this.pairingProtocol; try { const m3 = await protocol.buildPairSetupM3(tlv, pin); const m3Data = new Map(); m3Data.set(GattConstants.Types['HAP-Param-Value'], m3); m3Data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1])); const m3Pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, m3Data); let pdus = await connection.writeCharacteristic(characteristic, [m3Pdu]); if (pdus.length === 0) { throw new Error('M3: No response'); } let response = pdus[0]; let status = response.readUInt8(2); if (status !== 0) { throw new Error(`M3: Got error status: ${status}`); } if (response.length < 5) { const pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid); pdus = await connection.writeCharacteristic(characteristic, [pdu]); if (pdus.length === 0) { throw new Error('M4: No response'); } response = pdus[0]; status = response.readUInt8(2); if (status !== 0) { throw new Error(`M4: Got error status: ${status}`); } } const buffers = [pdus[0].slice(5, pdus[0].length)]; pdus.slice(1).map((p) => buffers.push(p.slice(2, p.length))); const m4Body = (0, tlv_1.decodeBuffer)(Buffer.concat(buffers)); if (!m4Body.has(GattConstants.Types['HAP-Param-Value'])) { throw new Error('M4: HAP-Param-Value missing'); } await this.pairingProtocol.parsePairSetupM4(m4Body.get(GattConstants.Types['HAP-Param-Value'])); if (!this.pairingProtocol.isTransientOnlyPairSetup()) { // According to specs for a transient pairSetup process no M5/6 is done, which should end in a // "non pairing" result and we miss AccessoryId and AccessoryLTPK, but the current session is // authenticated const m5 = await this.pairingProtocol.buildPairSetupM5(); const m5Data = new Map(); m5Data.set(GattConstants.Types['HAP-Param-Value'], m5); m5Data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1])); const m5Pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, m5Data); pdus = await connection.writeCharacteristic(characteristic, [m5Pdu]); if (pdus.length === 0) { throw new Error('M5: No response'); } response = pdus[0]; status = response.readUInt8(2); if (status !== 0) { throw new Error(`M5: Got error status: ${status}`); } if (response.length < 5) { const pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid); pdus = await connection.writeCharacteristic(characteristic, [pdu]); if (pdus.length === 0) { throw new Error('M6: No response'); } response = pdus[0]; status = response.readUInt8(2); if (status !== 0) { throw new Error(`M6: Got error status: ${status}`); } } const m6Body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length)); if (!m6Body.has(GattConstants.Types['HAP-Param-Value'])) { throw new Error('M6: HAP-Param-Value missing'); } await this.pairingProtocol.parsePairSetupM6(m6Body.get(GattConstants.Types['HAP-Param-Value'])); } // eslint-disable-next-line @typescript-eslint/no-empty-function await connection.disconnect().catch(() => { }); } catch (err) { // eslint-disable-next-line @typescript-eslint/no-empty-function await connection.disconnect().catch(() => { }); throw err; } }); } /** * Attempt to pair with a device. * * @param {string} pin - The pairing PIN, needs to be formatted as XXX-XX-XXX * @param {PairMethods} [pairMethod] - Method to use for pairing, default is PairSetupWithAuth * @param {PairingTypeFlags} [pairFlags] - Flags to use for Pairing for PairSetup * @returns {Promise} Promise which resolves when pairing is complete. */ async pairSetup(pin, pairMethod = pairing_protocol_1.PairMethods.PairSetupWithAuth, pairFlags = 0) { return await this.finishPairing(await this.startPairing(pairMethod, pairFlags), pin); } /** * Method used internally to generate session keys for a connection. * * @private * @param {GattConnection} connection - Existing GattConnection object * @returns {Promise} Promise which resolves when the pairing has been verified. */ async _pairVerify(connection) { return this._queuePairingOperation(async () => { debug('Start Pair-Verify process ...'); const serviceUuid = GattUtils.uuidToNobleUuid(Service.uuidFromService('public.hap.service.pairing')); const characteristicUuid = GattUtils.uuidToNobleUuid(Characteristic.uuidFromCharacteristic('public.hap.characteristic.pairing.pair-verify')); const { characteristics } = await new GattUtils.Watcher(this.peripheral, this.peripheral.discoverSomeServicesAndCharacteristicsAsync([serviceUuid], [characteristicUuid])).getPromise(); const characteristic = characteristics.find((c) => { return c.uuid === characteristicUuid; }); if (!characteristic) { throw new Error('pair-verify characteristic not found'); } const iid = await this._readInstanceId(characteristic); if (this.pairingProtocol.canResume()) { const m1 = await this.pairingProtocol.buildPairResumeM1(); const m1Data = new Map(); m1Data.set(GattConstants.Types['HAP-Param-Value'], m1); m1Data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1])); let pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, m1Data); let pdus = await connection.writeCharacteristic(characteristic, [pdu]); if (pdus.length === 0) { throw new Error('M1: No response'); } let response = pdus[0]; let status = response.readUInt8(2); if (status !== 0) { throw new Error(`M1: Got error status: ${status}`); } if (response.length < 5) { const pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid); pdus = await connection.writeCharacteristic(characteristic, [pdu]); if (pdus.length === 0) { throw new Error('M2: No response'); } const response = pdus[0]; const status = response.readUInt8(2); if (status !== 0) { throw new Error(`M2: Got error status: ${status}`); } } const m2Body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length)); if (!m2Body.has(GattConstants.Types['HAP-Param-Value'])) { throw new Error('M2: HAP-Param-Value missing'); } try { await this.pairingProtocol.parsePairResumeM2(m2Body.get(GattConstants.Types['HAP-Param-Value'])); } catch (_) { await this.pairingProtocol.parsePairVerifyM2(m2Body.get(GattConstants.Types['HAP-Param-Value'])); const m3 = await this.pairingProtocol.buildPairVerifyM3(); const m3Data = new Map(); m3Data.set(GattConstants.Types['HAP-Param-Value'], m3); m3Data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1])); pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, m3Data); pdus = await connection.writeCharacteristic(characteristic, [pdu]); if (pdus.length === 0) { throw new Error('M3: No response'); } response = pdus[0]; status = response.readUInt8(2); if (status !== 0) { throw new Error(`M3: Got error status: ${status}`); } if (response.length < 5) { pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid); pdus = await connection.writeCharacteristic(characteristic, [pdu]); if (pdus.length === 0) { throw new Error('M4: No response'); } response = pdus[0]; status = response.readUInt8(2); if (status !== 0) { throw new Error(`M4: Got error status: ${status}`); } } const m4Body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length)); if (!m4Body.has(GattConstants.Types['HAP-Param-Value'])) { throw new Error('M4: HAP-Param-Value missing'); } await this.pairingProtocol.parsePairVerifyM4(m4Body.get(GattConstants.Types['HAP-Param-Value'])); } } else { const m1 = await this.pairingProtocol.buildPairVerifyM1(); const m1Data = new Map(); m1Data.set(GattConstants.Types['HAP-Param-Value'], m1); m1Data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1])); let pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, m1Data); let pdus = await connection.writeCharacteristic(characteristic, [pdu]); if (pdus.length === 0) { throw new Error('M1: No response'); } let response = pdus[0]; let status = response.readUInt8(2); if (status !== 0) { throw new Error(`M1: Got error status: ${status}`); } if (response.length < 5) { pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid); pdus = await connection.writeCharacteristic(characteristic, [pdu]); if (pdus.length === 0) { throw new Error('M2: No response'); } response = pdus[0]; status = response.readUInt8(2); if (status !== 0) { throw new Error(`M2: Got error status: ${status}`); } } const m2Body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length)); if (!m2Body.has(GattConstants.Types['HAP-Param-Value'])) { throw new Error('M2: HAP-Param-Value missing'); } await this.pairingProtocol.parsePairVerifyM2(m2Body.get(GattConstants.Types['HAP-Param-Value'])); const m3 = await this.pairingProtocol.buildPairVerifyM3(); const m3Data = new Map(); m3Data.set(GattConstants.Types['HAP-Param-Value'], m3); m3Data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1])); pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, m3Data); pdus = await connection.writeCharacteristic(characteristic, [pdu]); if (pdus.length === 0) { throw new Error('M3: No response'); } response = pdus[0]; status = response.readUInt8(2); if (status !== 0) { throw new Error(`M3: Got error status: ${status}`); } if (response.length < 5) { pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid); pdus = await connection.writeCharacteristic(characteristic, [pdu]); if (pdus.length === 0) { throw new Error('M4: No response'); } response = pdus[0]; status = response.readUInt8(2); if (status !== 0) { throw new Error(`M4: Got error status: ${status}`); } } const m4Body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length)); if (!m4Body.has(GattConstants.Types['HAP-Param-Value'])) { throw new Error('M4: HAP-Param-Value missing'); } await this.pairingProtocol.parsePairVerifyM4(m4Body.get(GattConstants.Types['HAP-Param-Value'])); } const keys = await this.pairingProtocol.getSessionKeys(); connection.setSessionKeys(keys); debug('Finished Pair-Verify process ...'); }); } /** * Unpair the controller from a device. * * @param {string|Buffer} identifier - Identifier of the controller to remove * @returns {Promise} Promise which resolves when the process completes. */ removePairing(identifier) { const serviceUuid = GattUtils.uuidToNobleUuid(Service.uuidFromService('public.hap.service.pairing')); const characteristicUuid = GattUtils.uuidToNobleUuid(Characteristic.uuidFromCharacteristic('public.hap.characteristic.pairing.pairings')); return this._queueOperation(async () => { const connection = new gatt_connection_1.default(this.peripheral); try { if (typeof identifier === 'string') { identifier = pairing_protocol_1.default.bufferFromHex(identifier); } await connection.connect(); await this._pairVerify(connection); const { characteristics } = await new GattUtils.Watcher(this.peripheral, this.peripheral.discoverSomeServicesAndCharacteristicsAsync([serviceUuid], [characteristicUuid])).getPromise(); const characteristic = characteristics.find((c) => { return c.uuid === characteristicUuid; }); if (!characteristic) { throw new Error('pairings characteristic not found'); } const iid = await this._readInstanceId(characteristic); await this._pairVerify(connection); const packet = await this.pairingProtocol.buildRemovePairingM1(identifier); const data = new Map(); data.set(GattConstants.Types['HAP-Param-Value'], packet); data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1])); const pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, data); let pdus = await connection.writeCharacteristic(characteristic, [pdu]); if (pdus.length === 0) { throw new Error('M1: No response'); } let response = pdus[0]; let status = response.readUInt8(2); if (status !== 0) { throw new Error(`M1: Got error status: ${status}`); } if (response.length < 5) { const pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid); pdus = await connection.writeCharacteristic(characteristic, [pdu]); if (pdus.length === 0) { throw new Error('M2: No response'); } response = pdus[0]; status = response.readUInt8(2); if (status !== 0) { throw new Error(`M2: Got error status: ${status}`); } } const body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length)); if (!body.has(GattConstants.Types['HAP-Param-Value'])) { throw new Error('M2: HAP-Param-Value missing'); } await this.pairingProtocol.parseRemovePairingM2(body.get(GattConstants.Types['HAP-Param-Value'])); // eslint-disable-next-line @typescript-eslint/no-empty-function await connection.disconnect().catch(() => { }); } catch (err) { // eslint-disable-next-line @typescript-eslint/no-empty-function await connection.disconnect().catch(() => { }); throw err; } }); } /** * Add a pairing to a device. * * @param {string} identifier - Identifier of new controller * @param {Buffer} ltpk - Long-term public key of the new controller * @param {boolean} isAdmin - Whether or not the new controller is an admin * @returns {Promise} Promise which resolves when the process is complete. */ addPairing(identifier, ltpk, isAdmin) { const serviceUuid = GattUtils.uuidToNobleUuid(Service.uuidFromService('public.hap.service.pairing')); const characteristicUuid = GattUtils.uuidToNobleUuid(Characteristic.uuidFromCharacteristic('public.hap.characteristic.pairing.pairings')); return this._queueOperation(async () => { const connection = new gatt_connection_1.default(this.peripheral); try { await connection.connect(); const { characteristics } = await new GattUtils.Watcher(this.peripheral, this.peripheral.discoverSomeServicesAndCharacteristicsAsync([serviceUuid], [characteristicUuid])).getPromise(); const characteristic = characteristics.find((c) => { return c.uuid === characteristicUuid; }); if (!characteristic) { throw new Error('pairings characteristic not found'); } const iid = await this._readInstanceId(characteristic); await this._pairVerify(connection); const packet = await this.pairingProtocol.buildAddPairingM1(identifier, ltpk, isAdmin); const data = new Map(); data.set(GattConstants.Types['HAP-Param-Value'], packet); data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1])); let pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, data); let pdus = await connection.writeCharacteristic(characteristic, [pdu]); if (pdus.length === 0) { throw new Error('M1: No response'); } let response = pdus[0]; let status = response.readUInt8(2); if (status !== 0) { throw new Error(`M1: Got error status: ${status}`); } if (response.length < 5) { pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid); pdus = await connection.writeCharacteristic(characteristic, [pdu]); if (pdus.length === 0) { throw new Error('M2: No response'); } response = pdus[0]; status = response.readUInt8(2); if (status !== 0) { throw new Error(`M2: Got error status: ${status}`); } } const body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length)); if (!body.has(GattConstants.Types['HAP-Param-Value'])) { throw new Error('M2: HAP-Param-Value missing'); } await this.pairingProtocol.parseAddPairingM2(body.get(GattConstants.Types['HAP-Param-Value'])); // eslint-disable-next-line @typescript-eslint/no-empty-function await connection.disconnect().catch(() => { }); } catch (err) { // eslint-disable-next-line @typescript-eslint/no-empty-function await connection.disconnect(); throw err; } }); } /** * List the pairings on a device. * * @returns {Promise} Promise which resolves to the final TLV when the process * is complete. */ listPairings() { const serviceUuid = GattUtils.uuidToNobleUuid(Service.uuidFromService('public.hap.service.pairing')); const characteristicUuid = GattUtils.uuidToNobleUuid(Characteristic.uuidFromCharacteristic('public.hap.characteristic.pairing.pairings')); return this._queueOperation(async () => { const connection = new gatt_connection_1.default(this.peripheral); try { await connection.connect(); const { characteristics } = await new GattUtils.Watcher(this.peripheral, this.peripheral.discoverSomeServicesAndCharacteristicsAsync([serviceUuid], [characteristicUuid])).getPromise(); const characteristic = characteristics.find((c) => { return c.uuid === characteristicUuid; }); if (!characteristic) { throw new Error('pairings characteristic not found'); } const iid = await this._readInstanceId(characteristic); await this._pairVerify(connection); const packet = await this.pairingProtocol.buildListPairingsM1(); const data = new Map(); data.set(GattConstants.Types['HAP-Param-Value'], packet); data.set(GattConstants.Types['HAP-Param-Return-Response'], Buffer.from([1])); let pdu = this.gattProtocol.buildCharacteristicWriteRequest(this.getNextTransactionId(), iid, data); let pdus = await connection.writeCharacteristic(characteristic, [pdu]); if (pdus.length === 0) { throw new Error('M1: No response'); } let response = pdus[0]; let status = response.readUInt8(2); if (status !== 0) { throw new Error(`M1: Got error status: ${status}`); } if (response.length < 5) { pdu = this.gattProtocol.buildCharacteristicReadRequest(this.getNextTransactionId(), iid); pdus = await connection.writeCharacteristic(characteristic, [pdu]); if (pdus.length === 0) { throw new Error('M2: No response'); } response = pdus[0]; status = response.readUInt8(2); if (status !== 0) { throw new Error(`M2: Got error status: ${status}`); } } const body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length)); if (!body.has(GattConstants.Types['HAP-Param-Value'])) { throw new Error('M2: HAP-Param-Value missing'); } const tlv = await this.pairingProtocol.parseListPairingsM2(body.get(GattConstants.Types['HAP-Param-Value'])); // eslint-disable-next-line @typescript-eslint/no-empty-function await connection.disconnect().catch(() => { }); return tlv; } catch (err) { // eslint-disable-next-line @typescript-eslint/no-empty-function await connection.disconnect().catch(() => { }); throw err; } }); } /** * Get the accessory attribute database from a device. * * @returns {Promise} Promise which resolves to the JSON document. */ getAccessories() { const pairingUuid = GattUtils.uuidToNobleUuid(Service.uuidFromService('public.hap.service.pairing')); const protocolInformationUuid = GattUtils.uuidToNobleUuid(Service.uuidFromService('public.hap.service.protocol.information.service')); const serviceInstanceIdUuid = GattUtils.uuidToNobleUuid(GattConstants.ServiceInstanceIdUuid); const serviceSignatureUuid = GattUtils.uuidToNobleUuid(GattConstants.ServiceSignatureUuid); return this._queueOperation(async () => { const database = { accessories: [ { aid: 1, services: [], }, ], }; const connection = new gatt_connection_1.default(this.peripheral); try { await connection.connect(); const { services, characteristics: allCharacteristics } = await new GattUtils.Watcher(this.peripheral, this.peripheral.discoverAllServicesAndCharacteristicsAsync()).getPromise(); // Get the Service IIDs let queue = new queue_1.OpQueue(); let lastOp = Promise.resolve(); for (const service of services) { if (service.uuid === pairingUuid || service.uuid === protocolInformationUuid) { continue; } const characteristic = service.characteristics.find((c) => { return c.uuid === serviceInstanceIdUuid; }); if (!characteristic) { continue; } lastOp = queue.queue(async () => { const data = await new GattUtils.Watcher(this.peripheral, characteristic.readAsync()).getPromise(); database.accessories[0].services.push({ iid: data.readUInt16LE(0), type: GattUtils.nobleUuidToUuid(service.uuid), characteristics: service.characteristics .filter((c) => { return c.uuid !== serviceInstanceIdUuid && c.uuid !== serviceSignatureUuid; }) .map((c) => { return { type: GattUtils.nobleUuidToUuid(c.uuid), perms: [], format: 'data', }; }), }); }); } await lastOp; queue = new queue_1.OpQueue(); lastOp = Promise.resolve(); const characteristics = []; for (const characteristic of allCharacteristics) { lastOp = queue.queue(async () => { try { const iid = await this._readInstanceId(characteristic); const serviceType = GattUtils.nobleUuidToUuid(characteristic._serviceUuid); const characteristicType = GattUtils.nobleUuidToUuid(characteristic.uuid); for (const service of database.accessories[0].services) { if (service.type === serviceType) { for (const characteristic of service.characteristics) { if (characteristic.type === characteristicType) { characteristic.iid = iid; break; } } break; } } characteristics.push({ characteristic, iid }); } catch (_) { // Ignore errors here, as not all characteristics will have IIDs } }); } await lastOp; queue = new queue_1.OpQueue(); lastOp = Promise.resolve(); for (const c of characteristics) { const serviceUuid = GattUtils.nobleUuidToUuid(c.characteristic._serviceUuid); const characteristicUuid = GattUtils.nobleUuidToUuid(c.characteristic.uuid); if (characteristicUuid === GattConstants.ServiceSignatureUuid) { const service = database.accessories[0].services.find((s) => { return s.type === serviceUuid; }); if (!service) { continue; } const pdu = this.gattProtocol.buildServiceSignatureReadRequest(this.getNextTransactionId(), service.iid); lastOp = queue.queue(async () => { const pdus = await connection.writeCharacteristic(c.characteristic, [pdu]); if (pdus.length === 0) { return; } const response = pdus[0]; const body = (0, tlv_1.decodeBuffer)(response.slice(5, response.length)); const props = body.get(GattConstants.Types['HAP-Param-HAP-Service-Properties']); if (props && props.length) { switch (props.readUInt16LE(0)) { case 1: service.primary = true; break; case 2: service.hidden = true; break; } } const linked = body.get(GattConstants.Types['HAP-Param-HAP-Linked-Services']); if (linked && linked.length) { service.linked = []; for (let idx = 0; idx < linked.length; idx += 2) { service.linked.push(linked.readUInt16LE(idx)); } } }); } } await lastOp; await this._pairVerify(connection); const toFetch = []; for (const c of characteristics) { const serviceUuid = GattUtils.nobleUuidToUuid(c.characteristic._serviceUuid); const characteristicUuid = GattUtils.nobleUuidToUuid(c.characteristic.uuid); if (characteristicUuid !== GattConstants.ServiceSignatureUuid && c.characteristic._serviceUuid !== protocolInformationUuid && c.characteristic._serviceUuid !== pairingUuid) { toFetch.push({ serviceUuid, characteristicUuid, iid: c.iid, }); } } const list = await this.getCharacteristics(toFetch, { meta: true, perms: true, ev: true, type: true, extra: true, }, connection); for (const entry of list.characteristics) { if (!entry) { continue; } const service = database.accessories[0].services.find((s) => { return s.type === entry.serviceUuid; }); if (!service) { continue; } const characteristic = service.characteristics.find((c) => { return c.iid === entry.iid; }); if (!characteristic) { continue; } delete entry.serviceUuid; Object.assign(characteristic, entry); } // eslint-disable-next-line @typescript-eslint/no-empty-function await connection.disconnect().catch(() => { }); return database; } catch (err) { // eslint-disable-next-line @typescript-eslint/no-empty-function await connection.disconnect().catch(() => { }); throw err; } }); } /** * Read a set of characteristics. * * @param {Object[]} characteristics - Characteristics to get, as a list of * objects: {characteristicUuid, serviceUuid, iid, format} * @param {Object?} options - Options dictating what metadata to fetch * @param {Object?} connection - Existing GattConnection object, must already * be paired and verified * @returns {Promise} Promise which resolves to the JSON document. */ getCharacteristics(characteristics, options = {}, connection = null) { const skipQueue = connection !== null; const fn = async () => { options = Object.assign({ meta: false, perms: false, type: false, ev: false, extra: false, }, options); const cList = []; let needToClose = false; try { if (!connection) { needToClose = true;