@constructorfleet/ultimate-govee
Version:
Library for interacting with Govee devices written in Typescript.
357 lines • 16.2 kB
JavaScript
"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