UNPKG

@constructorfleet/ultimate-govee

Version:

Library for interacting with Govee devices written in Typescript.

357 lines 16.2 kB
"use strict"; 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 __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; 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 __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var BleClient_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.BleClient = void 0; const common_1 = require("@nestjs/common"); const child_process_1 = require("child_process"); const fs_1 = require("fs"); const promises_1 = require("fs/promises"); const json_stringify_safe_1 = __importDefault(require("json-stringify-safe")); const os_1 = require("os"); const rxjs_1 = require("rxjs"); const _ultimate_govee_common_1 = require("../../common"); const ble_options_1 = require("./ble.options"); const decoder_service_1 = require("./decoder/decoder.service"); const STATE_UNKNOWN = 'unknown'; const STATE_POWERED_ON = 'poweredOn'; let BleClient = BleClient_1 = class BleClient { set filterPeripherals(predicate) { this.peripheralFilter = predicate; } constructor(config, decoder) { this.config = config; this.decoder = decoder; this.logger = new common_1.Logger(BleClient_1.name); this.noble = undefined; this.seenNames = []; this.scanning = false; this.state = new rxjs_1.BehaviorSubject(STATE_UNKNOWN); this.connectedPeripheral = undefined; this.enabled = new rxjs_1.BehaviorSubject(false); this.peripheralDiscovered = new rxjs_1.Subject(); this.peripheralIds = new _ultimate_govee_common_1.DeltaMap(); this.peripheralAddresses = new Map(); this.cancelledCommands = []; this.peripheralDecoded = new rxjs_1.Subject(); this.commandQueue = new rxjs_1.Subject(); this.peripheralFilter = () => true; this.state.subscribe((state) => { if (state !== STATE_POWERED_ON && this.noble !== undefined) { try { this.noble?.removeAllListeners(); } catch (err) { this.logger.warn(err); } } }); this.enabled .subscribe(async () => this.enabled.getValue() ? await this.onEnabled() : await this.onDisabled()); (0, rxjs_1.interval)(10000) .pipe((0, rxjs_1.filter)(() => this.state.getValue() === STATE_POWERED_ON), (0, rxjs_1.filter)(() => this.enabled.getValue()), (0, rxjs_1.tap)(async () => await this.stopScanning()), (0, rxjs_1.tap)(() => { try { this.noble?.reset(); } catch (_) { // no-op } }), (0, rxjs_1.delay)(1000), (0, rxjs_1.filter)(() => this.enabled.getValue()), (0, rxjs_1.tap)(async () => await this.startScanning())) .subscribe(); this.peripheralDiscovered .pipe((0, rxjs_1.filter)((peripheral) => this.enabled.getValue() && peripheral !== undefined), (0, rxjs_1.filter)((peripheral) => this.peripheralFilter(peripheral)), (0, rxjs_1.concatMap)((peripheral) => (0, rxjs_1.from)(this.decodePeripheral(peripheral))), (0, rxjs_1.filter)((device) => device !== undefined), (0, rxjs_1.map)((device) => device)) .subscribe((event) => { if (event) { this.peripheralDecoded.next(event); this.logger.debug(`Decoded ${event.id}`); } }); this.commandQueue .pipe((0, rxjs_1.filter)(() => this.enabled.getValue()), (0, rxjs_1.filter)((command) => this.peripheralAddresses.has(command.address)), (0, rxjs_1.distinctUntilKeyChanged)('address')) .subscribe(async (command) => { if (this.cancelledCommands.includes(command.commandId)) { this.logger.debug('Command cancelled', { ...command, results$: 'subscription' }); return; } await this.sendCommand(command); }); } async decodePeripheral(peripheral) { await this.recordPeripheral(peripheral); const decodedDevice = await this.decoder.decodeDevice(peripheral); if (decodedDevice === undefined) { return undefined; } if ((decodedDevice.address ?? '').length === 0) { decodedDevice.address = this.peripheralIds.get(decodedDevice.id)?.address ?? ''; } return decodedDevice; } async recordPeripheral(peripheral) { const manufacturerData = peripheral.advertisement.manufacturerData?.toString('hex'); const data = [ `Name: ${peripheral.advertisement.localName}`, `Id: ${peripheral.id}`, `Address: ${peripheral.address}`, `Manufacturer Data: ${manufacturerData}`, `Service UUIDs: ${peripheral.advertisement.serviceUuids?.join(', ')}`, ]; this.logger.warn((0, json_stringify_safe_1.default)(data, null, 2)); const path = `ble/${manufacturerData?.substring(0, 4) ?? 'unknown'}`; if (!(0, fs_1.existsSync)(path)) { await (0, promises_1.mkdir)(path, { recursive: true }); } await (0, promises_1.writeFile)(`${path}/${peripheral.advertisement.localName ?? peripheral.address ?? peripheral.id}.txt`, data.join('\n'), { encoding: 'utf-8' }); if ((peripheral.advertisement?.localName ?? '').length === 0 || !/.*?((GV)|(GVH)|(H)[A-Z0-9]{4})_?[A-Z0-9]{4}.*/.exec(peripheral.advertisement.localName)) { return peripheral; } if (this.seenNames.includes(peripheral.advertisement?.localName)) { return peripheral; } if ((peripheral.address ?? '').length > 0 && !this.peripheralIds.has(peripheral.id)) { this.peripheralIds.set(peripheral.id, peripheral); this.peripheralAddresses.set(peripheral.address, peripheral.id); this.seenNames.push(peripheral.advertisement.localName); return peripheral; } try { await peripheral.connectAsync(); this.connectedPeripheral = peripheral; if ((0, os_1.platform)() === 'darwin') { const btData = (0, child_process_1.execSync)('system_profiler SPBluetoothDataType', { encoding: 'utf8', }); const btDataIndex = btData .split('\n') .findIndex((line) => line.includes(peripheral.advertisement.localName)); if (btDataIndex > 0) { peripheral.address = btData .split('\n')[btDataIndex + 1].trim() .replace('Address: ', ''); if (peripheral.address.length !== 0) { { this.logger.debug(`Got address ${peripheral.address} for ${peripheral.advertisement.localName}`); this.peripheralIds.set(peripheral.id, peripheral); this.peripheralAddresses.set(peripheral.address, peripheral.id); this.seenNames.push(peripheral.advertisement.localName); } } } } this.logger.log(`Disconnecting from ${peripheral.advertisement.localName}`); await peripheral.disconnectAsync(); this.connectedPeripheral = undefined; } catch (err) { this.logger.error(`Error while retrieving address: ${err}`); } finally { await this.startScanning(); } return peripheral; } async onDisabled() { this.logger.log('BLE disabled'); this.state.next(STATE_UNKNOWN); try { if (this.noble !== undefined) { this.noble?.removeAllListeners(); this.noble?.stopScanning(); } await this.connectedPeripheral?.disconnectAsync(); } catch (err) { this.logger.error('Error disabling BLE', err); } return false; } async onEnabled() { if (this.noble === undefined) { this.noble = await Promise.resolve().then(() => __importStar(require('@abandonware/noble'))).then((module) => module.default); } await (0, _ultimate_govee_common_1.sleep)(100); if (this.noble?.on === undefined) { this.logger.debug('Unable to import BLE library'); return; } this.logger.log('BLE enabled'); this.state.next(this.noble?._state); this.noble?.on('stateChange', (state) => { this.state.next(state); this.logger.warn(`State changed to ${state}`); }); this.noble?.on('scanStart', () => { this.logger.debug('Begin scanning'); this.scanning = true; }); this.noble?.on('scanStop', () => { this.logger.debug('Scanning stopped'); this.scanning = false; }); this.noble?.on('warning', (message) => this.logger.warn(message)); this.noble?.on('discover', (peripheral) => { this.peripheralDiscovered.next(peripheral); }); try { this.noble?.reset(); } catch (_) { // no-op } await this.startScanning(); return true; } async stopScanning() { if (this.enabled.getValue() && this.scanning) { this.logger.debug('Stop scanning'); await this.noble?.stopScanningAsync(); } } async startScanning() { if (this.enabled.getValue() && this.state.getValue() === STATE_POWERED_ON) { this.logger.debug('Start scanning'); return await this.noble?.startScanningAsync(); } } cancelCommand(commandId) { this.cancelledCommands.push(commandId); if (this.cancelledCommands.length > 100) { const overCount = this.cancelledCommands.length - 100; this.cancelledCommands.splice(0, overCount); } } async sendCommand({ id, address, commands, results$, debug, }) { if (!this.enabled.getValue()) { this.logger.warn(`Ble is disabled, unable to send command to ${id}`); return results$.complete(); } const peripheralId = this.peripheralAddresses.get(address); const peripheral = this.peripheralIds.get(peripheralId ?? ''); if (!peripheral) { this.logger.warn(`Device ${id} with address ${address} not yet discovered`); return results$.complete(); } try { // await this.stopScanning(); try { await peripheral.connectAsync(); this.connectedPeripheral = peripheral; } catch (err) { throw new Error(`Error connecting to ${id}`); } debug && this.logger.debug(`Connected to ${id}`); try { let serviceChars; let uuidSet; const uuids = [this.config.primary, this.config.secondary]; for (const uuidset of uuids) { if (uuidSet !== undefined) { break; } try { serviceChars = await peripheral.discoverSomeServicesAndCharacteristicsAsync([uuidset.serviceUUID], [uuidset.controlCharUUID, uuidset.dataCharUUID]); uuidSet = uuidset; } catch { // no-op } } if (uuidSet === undefined || serviceChars === undefined) { this.logger.warn('Unable to locate service with data characteristic'); return results$.complete(); } const dataChar = serviceChars.characteristics.find((c) => c.uuid === uuidSet.dataCharUUID); const writeChar = serviceChars.characteristics.find((c) => c.uuid === uuidSet.controlCharUUID); if (dataChar === undefined) { this.logger.warn(`Unable to locate service ${uuidSet.serviceUUID} with data characteristic ${uuidSet.dataCharUUID}`); return results$.complete(); } if (writeChar === undefined) { this.logger.warn(`Unable to locate service ${uuidSet.serviceUUID} with write characteristic ${uuidSet.controlCharUUID}`); return results$.complete(); } debug && this.logger.debug(`Sending ${commands.length} commands to ${id}`); dataChar.on('data', (data) => { results$.next(Array.from(new Uint8Array(data))); }); await dataChar.notifyAsync(true); await Promise.all(commands .map((command) => Buffer.from(new Uint8Array(command))) .map(async (command) => { await writeChar.writeAsync(command, true); await (0, _ultimate_govee_common_1.sleep)(100); })); await (0, _ultimate_govee_common_1.sleep)(300); await dataChar.notifyAsync(false); return results$.complete(); } catch (err) { throw new Error(`Error sending command to ${id}: ${err}`); } finally { await peripheral.disconnectAsync(); this.connectedPeripheral = undefined; } } catch (err) { this.logger.error(`Error while sending commands to ${id}`, err); return results$.complete(); } finally { // await this.startScanning(); } } }; exports.BleClient = BleClient; exports.BleClient = BleClient = BleClient_1 = __decorate([ (0, common_1.Injectable)(), __param(0, (0, common_1.Inject)(ble_options_1.BleConfig.KEY)), __metadata("design:paramtypes", [void 0, decoder_service_1.DecoderService]) ], BleClient); //# sourceMappingURL=ble.client.js.map