33 * ble.ts: SwitchBot v4.0.0 - BLE Discovery and Communication
44 */
55
6- import { Buffer } from 'node:buffer'
7-
86import type { BLEAdvertisement , BLEScanOptions , BLEServiceData } from './types/ble.js'
97
8+ import { Buffer } from 'node:buffer'
109import { createCipheriv } from 'node:crypto'
10+
1111import { EventEmitter } from 'node:events'
1212
1313import { BLENotAvailableError , CommandFailedError , DeviceNotFoundError } from './errors.js'
1414import { BLE_COMMAND_TIMEOUT , BLE_CONNECT_TIMEOUT , BLE_NOTIFY_CHARACTERISTIC_UUID , BLE_SCAN_TIMEOUT , BLE_SERVICE_UUID , BLE_WRITE_CHARACTERISTIC_UUID , DEVICE_MODEL_MAP } from './settings.js'
15- import { Logger , macToDeviceId , normalizeMAC , withTimeout , extractMacFromManufacturerData , mergeAdvertisement } from './utils/index.js'
15+ import { extractMacFromManufacturerData , Logger , macToDeviceId , mergeAdvertisement , normalizeMAC , withTimeout } from './utils/index.js'
16+ // Move RegExp to module scope to avoid re-compilation
17+ const CHARACTERISTIC_REGEX = / c h a r a c t e r i s t i c / i
18+ const UUID_DASH_REGEX = / - / g
1619
1720/**
1821 * BLE Scanner for discovering SwitchBot devices
@@ -81,7 +84,7 @@ export class BLEScanner extends EventEmitter {
8184 if ( ! this . noble ) {
8285 await this . initializeNoble ( )
8386 }
84-
87+
8588 if ( ! this . noble ) {
8689 throw new BLENotAvailableError ( 'BLE not available - noble failed to initialize' )
8790 }
@@ -188,11 +191,11 @@ export class BLEScanner extends EventEmitter {
188191
189192 // SwitchBot service UUID (current: fd3d, legacy: 000d)
190193 const uuid = typeof serviceDataItem . uuid === 'string' ? serviceDataItem . uuid . toLowerCase ( ) : ''
191- const isSwitchBotUUID = uuid === 'fd3d' ||
192- uuid === '0000fd3d-0000-1000-8000-00805f9b34fb' ||
193- uuid === '000d' ||
194- uuid === '0000000d-0000-1000-8000-00805f9b34fb'
195-
194+ const isSwitchBotUUID = uuid === 'fd3d'
195+ || uuid === '0000fd3d-0000-1000-8000-00805f9b34fb'
196+ || uuid === '000d'
197+ || uuid === '0000000d-0000-1000-8000-00805f9b34fb'
198+
196199 if ( ! isSwitchBotUUID ) {
197200 continue
198201 }
@@ -222,7 +225,6 @@ export class BLEScanner extends EventEmitter {
222225 continue
223226 }
224227
225-
226228 // Determine if advertisement is encrypted (simple heuristic: check for known encrypted models or data length)
227229 // This can be refined as needed
228230 const isEncrypted = ! ! ( serviceData && typeof serviceData . model === 'string' && serviceData . model . startsWith ( '!' ) )
@@ -349,7 +351,7 @@ export class BLEScanner extends EventEmitter {
349351 const { duration = BLE_SCAN_TIMEOUT , active = true } = options
350352
351353 this . logger . info ( 'Starting BLE scan' , { duration, active } )
352-
354+
353355 if ( ! this . noble ) {
354356 throw new BLENotAvailableError ( 'BLE not available - noble failed to initialize' )
355357 }
@@ -407,7 +409,7 @@ export class BLEScanner extends EventEmitter {
407409 * Get all discovered devices
408410 */
409411 getDiscoveredDevices ( ) : BLEAdvertisement [ ] {
410- return Array . from ( this . discoveredDevices . values ( ) )
412+ return [ ... this . discoveredDevices . values ( ) ]
411413 }
412414
413415 /**
@@ -452,8 +454,8 @@ export class BLEScanner extends EventEmitter {
452454 new Promise < BLEAdvertisement > ( ( resolve ) => {
453455 const handler = ( advertisement : BLEAdvertisement ) => {
454456 // Match by address (if available) or by ID
455- const matches = ( advertisement . address && normalizeMAC ( advertisement . address ) === normalizedMac ) ||
456- ( bleId && advertisement . id === bleId )
457+ const matches = ( advertisement . address && normalizeMAC ( advertisement . address ) === normalizedMac )
458+ || ( bleId && advertisement . id === bleId )
457459 if ( matches ) {
458460 this . off ( 'discover' , handler )
459461 resolve ( advertisement )
@@ -542,7 +544,7 @@ export class BLEConnection {
542544 if ( ! this . noble ) {
543545 await this . initializeNoble ( )
544546 }
545-
547+
546548 if ( ! this . noble ) {
547549 throw new BLENotAvailableError ( 'BLE not available - noble failed to initialize' )
548550 }
@@ -692,23 +694,24 @@ export class BLEConnection {
692694 notificationTimeoutMs = 5000 ,
693695 } = options
694696
695-
696697 return this . withMacLock ( normalizedMac , async ( ) => {
697698 const payload = this . encryptIfConfigured ( normalizedMac , data )
698699 await this . write ( normalizedMac , payload )
699700
700701 if ( expectNotification ) {
701702 // DEBUG: Log future creation
702- process . stdout . write ( `[DEBUG] Creating notification future for ${ normalizedMac } \n ` )
703+ this . logger . debug ( `[DEBUG] Creating notification future for ${ normalizedMac } ` )
703704 // Wait for notification (per-command future)
704705 return await new Promise < Buffer > ( ( resolve , reject ) => {
705706 if ( this . notificationFutures . has ( normalizedMac ) ) {
706707 this . logger . warn ( `Notification future already exists for ${ normalizedMac } , overwriting` )
707708 const prev = this . notificationFutures . get ( normalizedMac )
708- if ( prev ) clearTimeout ( prev . timer )
709+ if ( prev ) {
710+ clearTimeout ( prev . timer )
711+ }
709712 }
710713 const timer = setTimeout ( ( ) => {
711- process . stdout . write ( `[DEBUG] Notification future timed out for ${ normalizedMac } \n ` )
714+ this . logger . debug ( `[DEBUG] Notification future timed out for ${ normalizedMac } ` )
712715 this . notificationFutures . delete ( normalizedMac )
713716 reject ( new Error ( `Notification timeout (${ notificationTimeoutMs } ms) for ${ normalizedMac } ` ) )
714717 } , notificationTimeoutMs )
@@ -717,7 +720,7 @@ export class BLEConnection {
717720 if ( typeof ( this as any ) . _onNotificationFutureSet === 'function' ) {
718721 ( this as any ) . _onNotificationFutureSet ( normalizedMac )
719722 }
720- process . stdout . write ( `[DEBUG] notificationFutures after set: ${ Array . from ( this . notificationFutures . keys ( ) ) . join ( ',' ) } \n ` )
723+ this . logger . debug ( `[DEBUG] notificationFutures after set: ${ [ ... this . notificationFutures . keys ( ) ] . join ( ',' ) } ` )
721724 } )
722725 }
723726
@@ -751,28 +754,27 @@ export class BLEConnection {
751754 throw new CommandFailedError ( `Notify characteristic not available for ${ normalizedMac } ` , 'ble' )
752755 }
753756
754-
755757 if ( ! this . notificationHandlers . has ( normalizedMac ) ) {
756758 this . notificationHandlers . set ( normalizedMac , new Set ( ) )
757759
758760 if ( typeof chars . notify . on === 'function' ) {
759761 chars . notify . on ( 'data' , ( payload : Buffer ) => {
760762 // DEBUG: Log notification future state
761- process . stdout . write ( `[DEBUG] notifyChar handler called for ${ normalizedMac } \n ` )
762- process . stdout . write ( `[DEBUG] notificationFutures keys at handler: ${ Array . from ( this . notificationFutures . keys ( ) ) . join ( ',' ) } \n ` )
763+ this . logger . debug ( `[DEBUG] notifyChar handler called for ${ normalizedMac } ` )
764+ this . logger . debug ( `[DEBUG] notificationFutures keys at handler: ${ [ ... this . notificationFutures . keys ( ) ] . join ( ',' ) } ` )
763765 const future = this . notificationFutures . get ( normalizedMac )
764766 if ( future ) {
765- process . stdout . write ( `[DEBUG] notificationFutures present, resolving as solicited\n ` )
767+ this . logger . debug ( `[DEBUG] notificationFutures present, resolving as solicited` )
766768 clearTimeout ( future . timer )
767769 this . notificationFutures . delete ( normalizedMac )
768- process . stdout . write ( `[DEBUG] notificationFutures after delete: ${ Array . from ( this . notificationFutures . keys ( ) ) . join ( ',' ) } \n ` )
770+ this . logger . debug ( `[DEBUG] notificationFutures after delete: ${ [ ... this . notificationFutures . keys ( ) ] . join ( ',' ) } ` )
769771 future . resolve ( payload )
770772 return
771773 }
772- process . stdout . write ( `[DEBUG] unsolicited notification branch\n ` )
773- process . stdout . write ( `[DEBUG] about to call logger.info for unsolicited notification\n ` )
774+ this . logger . debug ( `[DEBUG] unsolicited notification branch` )
775+ this . logger . debug ( `[DEBUG] about to call logger.info for unsolicited notification` )
774776 this . logger . info ( `Unsolicited notification from ${ normalizedMac } : ${ payload . toString ( 'hex' ) } ` )
775- process . stdout . write ( `[DEBUG] after logger.info for unsolicited notification\n ` )
777+ this . logger . debug ( `[DEBUG] after logger.info for unsolicited notification` )
776778 const handlers = this . notificationHandlers . get ( normalizedMac )
777779 if ( ! handlers ) {
778780 return
@@ -905,8 +907,8 @@ export class BLEConnection {
905907 'Characteristic discovery timed out' ,
906908 )
907909
908- const writeChar = characteristics . find ( ( c : any ) => c . uuid === BLE_WRITE_CHARACTERISTIC_UUID . replace ( / - / g , '' ) )
909- const notifyChar = characteristics . find ( ( c : any ) => c . uuid === BLE_NOTIFY_CHARACTERISTIC_UUID . replace ( / - / g , '' ) )
910+ const writeChar = characteristics . find ( ( c : any ) => c . uuid === BLE_WRITE_CHARACTERISTIC_UUID . replace ( UUID_DASH_REGEX , '' ) )
911+ const notifyChar = characteristics . find ( ( c : any ) => c . uuid === BLE_NOTIFY_CHARACTERISTIC_UUID . replace ( UUID_DASH_REGEX , '' ) )
910912
911913 if ( ! writeChar || ! notifyChar ) {
912914 throw new Error ( 'Required characteristics not found' )
@@ -994,11 +996,13 @@ export class BLEConnection {
994996 )
995997 } catch ( err : any ) {
996998 // If error is characteristic-related, clear cache and retry once
997- if ( / c h a r a c t e r i s t i c / i . test ( err ?. message || '' ) ) {
999+ if ( CHARACTERISTIC_REGEX . test ( err ?. message || '' ) ) {
9981000 this . characteristics . delete ( normalizedMac )
9991001 await this . discoverCharacteristics ( normalizedMac , this . connections . get ( normalizedMac ) )
10001002 chars = this . characteristics . get ( normalizedMac )
1001- if ( ! chars ) throw err
1003+ if ( ! chars ) {
1004+ throw err
1005+ }
10021006 // Retry once
10031007 await withTimeout (
10041008 new Promise < void > ( ( resolve , reject ) => {
@@ -1059,11 +1063,13 @@ export class BLEConnection {
10591063 )
10601064 } catch ( err : any ) {
10611065 // If error is characteristic-related, clear cache and retry once
1062- if ( / c h a r a c t e r i s t i c / i . test ( err ?. message || '' ) ) {
1066+ if ( CHARACTERISTIC_REGEX . test ( err ?. message || '' ) ) {
10631067 this . characteristics . delete ( normalizedMac )
10641068 await this . discoverCharacteristics ( normalizedMac , this . connections . get ( normalizedMac ) )
10651069 chars = this . characteristics . get ( normalizedMac )
1066- if ( ! chars ) throw err
1070+ if ( ! chars ) {
1071+ throw err
1072+ }
10671073 // Retry once
10681074 return await withTimeout (
10691075 new Promise < Buffer > ( ( resolve , reject ) => {
@@ -1096,7 +1102,7 @@ export class BLEConnection {
10961102 * Disconnect all devices
10971103 */
10981104 async disconnectAll ( ) : Promise < void > {
1099- const macs = Array . from ( this . connections . keys ( ) )
1105+ const macs = [ ... this . connections . keys ( ) ]
11001106 await Promise . all ( macs . map ( mac => this . disconnect ( mac ) ) )
11011107 }
11021108
0 commit comments