hap-controller
Version:
Library to implement a HAP (HomeKit) controller
208 lines • 7.24 kB
JavaScript
"use strict";
/**
* BLE discovery wrappers for finding HAP devices.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DiscoveryPairingStatusFlags = void 0;
const events_1 = require("events");
const gatt_client_1 = __importDefault(require("./gatt-client"));
const debug_1 = __importDefault(require("debug"));
let noble = null;
try {
noble = require('@stoprocent/noble');
if (typeof (noble === null || noble === void 0 ? void 0 : noble.on) !== 'function') {
// The following commit broke the default exported instance of noble:
// https://github.com/abandonware/noble/commit/b67eea246f719947fc45b1b52b856e61637a8a8e
noble = noble({ extended: false });
}
}
catch (error) {
// Ignore
}
const debug = (0, debug_1.default)('hap-controller:gatt-client');
/**
* See Table 7-43
*/
const DiscoveryPairingStatusFlags = {
AccessoryNotPaired: 0x01,
};
exports.DiscoveryPairingStatusFlags = DiscoveryPairingStatusFlags;
/**
* Handle discovery of IP devices
*
* @fires BLEDiscovery#serviceUp
* @fires BLEDiscovery#serviceChanged
*/
class BLEDiscovery extends events_1.EventEmitter {
constructor() {
super();
this.scanEnabled = false;
this.allowDuplicates = false;
this.services = new Map();
this.handleStateChange = this._handleStateChange.bind(this);
this.handleDiscover = this._handleDiscover.bind(this);
this.handleScanStart = this._handleScanStart.bind(this);
this.handleScanStop = this._handleScanStop.bind(this);
}
/**
* Start searching for BLE HAP devices.
*
* @param {boolean} allowDuplicates - Deprecated, use new serviceChanged event instead.
* Allow duplicate serviceUp events. This
* is needed for disconnected events, where the GSN is
* updated in the advertisement.
*/
start(allowDuplicates = false) {
if (!noble) {
throw new Error('BLE could not be enabled or no device found');
}
this.scanEnabled = true;
this.allowDuplicates = allowDuplicates;
noble.on('stateChange', this.handleStateChange);
noble.on('scanStart', this.handleScanStart);
noble.on('scanStop', this.handleScanStop);
noble.on('discover', this.handleDiscover);
// Only manually start if powered on already. Otherwise, wait for state
// change and handle it there.
if (noble._state === 'poweredOn') {
noble.startScanning([], true);
}
}
/**
* Get PairMethod to use for pairing from the data received during discovery
*
* @param {HapServiceBle} service Discovered service object to check
* @returns {Promise<number>} Promise which resolves with the PairMethod to use
*/
async getPairMethod(service) {
const client = new gatt_client_1.default(service.DeviceID, service.peripheral);
return client.getPairingMethod();
}
/**
* List the currently known services.
*
* @returns {Object[]} Array of services
*/
list() {
return Array.from(this.services.values());
}
/**
* Stop an ongoing discovery process.
*/
stop() {
if (!noble) {
throw new Error('BLE could not be enabled or no device found');
}
this.scanEnabled = false;
noble.stopScanning();
noble.removeListener('stateChange', this.handleStateChange);
noble.removeListener('scanStart', this.handleScanStart);
noble.removeListener('scanStop', this.handleScanStop);
noble.removeListener('discover', this.handleDiscover);
}
_handleStateChange(state) {
if (!noble) {
return;
}
if (state === 'poweredOn' && this.scanEnabled) {
noble.startScanning([], true);
}
else {
noble.stopScanning();
}
}
_handleScanStart() {
if (!noble) {
return;
}
if (!this.scanEnabled) {
noble.stopScanning();
}
}
_handleScanStop() {
if (!noble) {
return;
}
if (this.scanEnabled && noble._state === 'poweredOn') {
noble.startScanning([], true);
}
}
_handleDiscover(peripheral) {
const advertisement = peripheral.advertisement;
const manufacturerData = advertisement.manufacturerData;
if (!advertisement || !advertisement.localName || !manufacturerData || manufacturerData.length < 17) {
return;
}
// See Chapter 6.4.2.2
const localName = advertisement.localName;
const CoID = manufacturerData.readUInt16LE(0);
const TY = manufacturerData.readUInt8(2);
const AIL = manufacturerData.readUInt8(3);
const SF = manufacturerData.readUInt8(4);
const deviceID = manufacturerData.slice(5, 11);
const ACID = manufacturerData.readUInt16LE(11);
const GSN = manufacturerData.readUInt16LE(13);
const CN = manufacturerData.readUInt8(15);
const CV = manufacturerData.readUInt8(16);
// const SH = manufacturerData.length > 17 ? manufacturerData.slice(17, 21) : Buffer.alloc(0);
if (TY === 0x11) {
debug(`Encrypted Broadcast detected ... ignoring for now: ${manufacturerData}`);
}
if (CoID !== 0x4c || TY !== 0x06 || CV !== 0x02) {
return;
}
let formattedId = '';
for (const b of deviceID) {
formattedId += `${b.toString(16).padStart(2, '0')}:`;
}
formattedId = formattedId.substr(0, 17);
const service = {
name: localName,
CoID,
TY,
AIL,
SF,
DeviceID: formattedId,
ACID,
GSN,
CN,
CV,
peripheral,
// SH,
'c#': CN,
id: formattedId,
ci: ACID,
availableToPair: !!(SF & DiscoveryPairingStatusFlags.AccessoryNotPaired),
};
const formerService = this.services.get(service.DeviceID);
this.services.set(service.DeviceID, service);
if (formerService && !this.allowDuplicates) {
for (const el of Object.keys(service)) {
if (el !== 'peripheral' && el !== 'name' && formerService[el] !== service[el]) {
/**
* Device data changed event
*
* @event BLEDiscovery#serviceChanged
* @type HapServiceBle
*/
this.emit('serviceChanged', service);
break;
}
}
}
else {
/**
* New device discovered event
*
* @event BLEDiscovery#serviceUp
* @type HapServiceBle
*/
this.emit('serviceUp', service);
}
}
}
exports.default = BLEDiscovery;
//# sourceMappingURL=ble-discovery.js.map