Skip to content

Commit e34d580

Browse files
committed
more device support
1 parent 2176af8 commit e34d580

File tree

10 files changed

+652
-64
lines changed

10 files changed

+652
-64
lines changed

src/ble.ts

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@
33
* ble.ts: SwitchBot v4.0.0 - BLE Discovery and Communication
44
*/
55

6-
import { Buffer } from 'node:buffer'
7-
86
import type { BLEAdvertisement, BLEScanOptions, BLEServiceData } from './types/ble.js'
97

8+
import { Buffer } from 'node:buffer'
109
import { createCipheriv } from 'node:crypto'
10+
1111
import { EventEmitter } from 'node:events'
1212

1313
import { BLENotAvailableError, CommandFailedError, DeviceNotFoundError } from './errors.js'
1414
import { 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 = /characteristic/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 (/characteristic/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 (/characteristic/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

src/devices/wo-ai-hub.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,43 @@
1-
import type { DeviceStatus } from '../types/index.js'
1+
import type { HubStatus } from '../types/device.js'
22
import { SwitchBotDevice } from './base.js'
33

44
export class WoAIHub extends SwitchBotDevice {
5-
async getStatus(): Promise<DeviceStatus> {
6-
throw new Error('WoAIHub.getStatus not implemented')
5+
/**
6+
* Get device status (BLE first, then API)
7+
*/
8+
async getStatus(): Promise<HubStatus> {
9+
try {
10+
// Try BLE first
11+
if (this.hasBLE()) {
12+
const bleData = await this.getBLEStatus()
13+
return {
14+
deviceId: this.info.id,
15+
connectionType: 'ble',
16+
temperature: bleData.temperature,
17+
humidity: bleData.humidity,
18+
lightLevel: bleData.lightLevel,
19+
updatedAt: new Date(),
20+
}
21+
}
22+
23+
// Fallback to API
24+
if (this.hasAPI()) {
25+
const apiStatus = await this.getAPIStatus()
26+
return {
27+
deviceId: this.info.id,
28+
connectionType: 'api',
29+
temperature: apiStatus.temperature,
30+
humidity: apiStatus.humidity,
31+
lightLevel: apiStatus.lightLevel,
32+
version: apiStatus.version,
33+
updatedAt: new Date(),
34+
}
35+
}
36+
37+
throw new Error('No connection method available')
38+
} catch (error) {
39+
this.logger.error('Failed to get status', error)
40+
throw error
41+
}
742
}
843
}

0 commit comments

Comments
 (0)