node-switchbot 4.0.0-beta.1 → 4.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +19 -5
- package/BLE.md +117 -4
- package/CHANGELOG.md +45 -0
- package/README.md +7 -1
- package/dist/api.d.ts +3 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +9 -6
- package/dist/api.js.map +1 -1
- package/dist/ble.d.ts +38 -4
- package/dist/ble.d.ts.map +1 -1
- package/dist/ble.js +409 -53
- package/dist/ble.js.map +1 -1
- package/dist/devices/base.d.ts +83 -5
- package/dist/devices/base.d.ts.map +1 -1
- package/dist/devices/base.js +371 -34
- package/dist/devices/base.js.map +1 -1
- package/dist/devices/device-override-state-during-connection.d.ts +27 -0
- package/dist/devices/device-override-state-during-connection.d.ts.map +1 -0
- package/dist/devices/device-override-state-during-connection.js +45 -0
- package/dist/devices/device-override-state-during-connection.js.map +1 -0
- package/dist/devices/index.d.ts +4 -0
- package/dist/devices/index.d.ts.map +1 -1
- package/dist/devices/index.js +4 -0
- package/dist/devices/index.js.map +1 -1
- package/dist/devices/sequence-device.d.ts +36 -0
- package/dist/devices/sequence-device.d.ts.map +1 -0
- package/dist/devices/sequence-device.js +75 -0
- package/dist/devices/sequence-device.js.map +1 -0
- package/dist/devices/wo-air-purifier.d.ts +2 -2
- package/dist/devices/wo-air-purifier.d.ts.map +1 -1
- package/dist/devices/wo-air-purifier.js +2 -2
- package/dist/devices/wo-air-purifier.js.map +1 -1
- package/dist/devices/wo-bulb.d.ts +10 -0
- package/dist/devices/wo-bulb.d.ts.map +1 -1
- package/dist/devices/wo-bulb.js +69 -0
- package/dist/devices/wo-bulb.js.map +1 -1
- package/dist/devices/wo-curtain.d.ts +3 -3
- package/dist/devices/wo-curtain.d.ts.map +1 -1
- package/dist/devices/wo-curtain.js +12 -9
- package/dist/devices/wo-curtain.js.map +1 -1
- package/dist/devices/wo-hand.d.ts +53 -2
- package/dist/devices/wo-hand.d.ts.map +1 -1
- package/dist/devices/wo-hand.js +121 -3
- package/dist/devices/wo-hand.js.map +1 -1
- package/dist/devices/wo-lock-pro.d.ts +11 -2
- package/dist/devices/wo-lock-pro.d.ts.map +1 -1
- package/dist/devices/wo-lock-pro.js +65 -3
- package/dist/devices/wo-lock-pro.js.map +1 -1
- package/dist/devices/wo-lock.d.ts +7 -2
- package/dist/devices/wo-lock.d.ts.map +1 -1
- package/dist/devices/wo-lock.js +58 -3
- package/dist/devices/wo-lock.js.map +1 -1
- package/dist/devices/wo-plug-mini-us.d.ts +2 -2
- package/dist/devices/wo-plug-mini-us.d.ts.map +1 -1
- package/dist/devices/wo-plug-mini-us.js +2 -2
- package/dist/devices/wo-plug-mini-us.js.map +1 -1
- package/dist/devices/wo-relay-switch-1.d.ts +4 -2
- package/dist/devices/wo-relay-switch-1.d.ts.map +1 -1
- package/dist/devices/wo-relay-switch-1.js +36 -4
- package/dist/devices/wo-relay-switch-1.js.map +1 -1
- package/dist/devices/wo-relay-switch-2pm.d.ts +21 -0
- package/dist/devices/wo-relay-switch-2pm.d.ts.map +1 -0
- package/dist/devices/wo-relay-switch-2pm.js +39 -0
- package/dist/devices/wo-relay-switch-2pm.js.map +1 -0
- package/dist/devices/wo-rgbic-bulb.d.ts +29 -0
- package/dist/devices/wo-rgbic-bulb.d.ts.map +1 -0
- package/dist/devices/wo-rgbic-bulb.js +84 -0
- package/dist/devices/wo-rgbic-bulb.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/settings.d.ts +17 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +18 -1
- package/dist/settings.js.map +1 -1
- package/dist/switchbot.d.ts.map +1 -1
- package/dist/switchbot.js +31 -8
- package/dist/switchbot.js.map +1 -1
- package/dist/types/ble.d.ts +20 -1
- package/dist/types/ble.d.ts.map +1 -1
- package/dist/types/ble.js.map +1 -1
- package/dist/types/device.d.ts +30 -3
- package/dist/types/device.d.ts.map +1 -1
- package/dist/types/index.d.ts +22 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/bot-ble.d.ts +36 -0
- package/dist/utils/bot-ble.d.ts.map +1 -0
- package/dist/utils/bot-ble.js +109 -0
- package/dist/utils/bot-ble.js.map +1 -0
- package/dist/utils/circuit-breaker.d.ts +98 -0
- package/dist/utils/circuit-breaker.d.ts.map +1 -0
- package/dist/utils/circuit-breaker.js +187 -0
- package/dist/utils/circuit-breaker.js.map +1 -0
- package/dist/utils/connection-tracker.d.ts +66 -0
- package/dist/utils/connection-tracker.d.ts.map +1 -0
- package/dist/utils/connection-tracker.js +184 -0
- package/dist/utils/connection-tracker.js.map +1 -0
- package/dist/utils/fallback-handler.d.ts +68 -0
- package/dist/utils/fallback-handler.d.ts.map +1 -0
- package/dist/utils/fallback-handler.js +131 -0
- package/dist/utils/fallback-handler.js.map +1 -0
- package/dist/utils/index.d.ts +10 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +41 -4
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/retry.d.ts +55 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +95 -0
- package/dist/utils/retry.js.map +1 -0
- package/docs/assets/hierarchy.js +1 -1
- package/docs/assets/navigation.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/APIError.html +2 -2
- package/docs/classes/APINotAvailableError.html +2 -2
- package/docs/classes/BLEConnection.html +16 -10
- package/docs/classes/BLENotAvailableError.html +2 -2
- package/docs/classes/BLEScanner.html +11 -9
- package/docs/classes/CommandFailedError.html +2 -2
- package/docs/classes/ConnectionTimeoutError.html +2 -2
- package/docs/classes/DeviceManager.html +13 -13
- package/docs/classes/DeviceNotFoundError.html +2 -2
- package/docs/classes/DeviceOverrideStateDuringConnection.html +56 -0
- package/docs/classes/DiscoveryError.html +2 -2
- package/docs/classes/OpenAPIClient.html +24 -24
- package/docs/classes/SequenceDevice.html +58 -0
- package/docs/classes/SwitchBot.html +11 -11
- package/docs/classes/SwitchBotDevice.html +43 -15
- package/docs/classes/SwitchBotError.html +2 -2
- package/docs/classes/ValidationError.html +2 -2
- package/docs/classes/WoAirPurifier.html +48 -18
- package/docs/classes/WoAirPurifierTable.html +48 -18
- package/docs/classes/WoBlindTilt.html +48 -20
- package/docs/classes/WoBulb.html +52 -19
- package/docs/classes/WoCeilingLight.html +52 -19
- package/docs/classes/WoContact.html +42 -14
- package/docs/classes/WoCurtain.html +46 -18
- package/docs/classes/WoHand.html +63 -19
- package/docs/classes/WoHub2.html +42 -14
- package/docs/classes/WoHub3.html +42 -14
- package/docs/classes/WoHumi.html +46 -18
- package/docs/classes/WoHumi2.html +46 -18
- package/docs/classes/WoIOSensorTH.html +42 -14
- package/docs/classes/WoKeypad.html +42 -14
- package/docs/classes/WoLeak.html +42 -14
- package/docs/classes/WoPlugMiniJP.html +45 -17
- package/docs/classes/WoPlugMiniUS.html +45 -17
- package/docs/classes/WoPresence.html +42 -14
- package/docs/classes/WoRelaySwitch1.html +47 -17
- package/docs/classes/WoRelaySwitch1PM.html +47 -17
- package/docs/classes/WoRemote.html +42 -14
- package/docs/classes/WoSensorTH.html +42 -14
- package/docs/classes/WoSensorTHPlus.html +42 -14
- package/docs/classes/WoSensorTHPro.html +42 -14
- package/docs/classes/WoSensorTHProCO2.html +42 -14
- package/docs/classes/WoSmartLock.html +49 -16
- package/docs/classes/WoSmartLockPro.html +52 -17
- package/docs/classes/WoStrip.html +52 -19
- package/docs/enums/LogLevel.html +2 -2
- package/docs/enums/SwitchBotBLEModel.html +2 -2
- package/docs/enums/SwitchBotBLEModelName.html +2 -2
- package/docs/functions/updateBaseURL.html +1 -1
- package/docs/hierarchy.html +1 -1
- package/docs/index.html +2 -2
- package/docs/interfaces/APICommandRequest.html +2 -2
- package/docs/interfaces/APICommandResponse.html +2 -2
- package/docs/interfaces/APIDevice.html +2 -2
- package/docs/interfaces/APIDeviceStatus.html +2 -2
- package/docs/interfaces/APIErrorResponse.html +2 -2
- package/docs/interfaces/APIResponse.html +2 -2
- package/docs/interfaces/AirPurifierCommands.html +2 -2
- package/docs/interfaces/AirPurifierServiceData.html +20 -5
- package/docs/interfaces/AirPurifierStatus.html +7 -7
- package/docs/interfaces/BLEAdvertisement.html +3 -2
- package/docs/interfaces/BLEScanOptions.html +5 -5
- package/docs/interfaces/BLEServiceData.html +22 -5
- package/docs/interfaces/BlindTiltCommands.html +2 -2
- package/docs/interfaces/BlindTiltServiceData.html +21 -5
- package/docs/interfaces/BlindTiltStatus.html +6 -6
- package/docs/interfaces/BotCommands.html +6 -2
- package/docs/interfaces/BotServiceData.html +20 -5
- package/docs/interfaces/BotStatus.html +6 -6
- package/docs/interfaces/BulbCommands.html +4 -2
- package/docs/interfaces/BulbServiceData.html +21 -5
- package/docs/interfaces/BulbStatus.html +6 -6
- package/docs/interfaces/CeilingLightCommands.html +4 -2
- package/docs/interfaces/CeilingLightServiceData.html +21 -5
- package/docs/interfaces/CeilingLightStatus.html +6 -6
- package/docs/interfaces/CommandResult.html +6 -6
- package/docs/interfaces/ContactServiceData.html +22 -5
- package/docs/interfaces/ContactStatus.html +6 -6
- package/docs/interfaces/CurtainCommands.html +2 -2
- package/docs/interfaces/CurtainServiceData.html +22 -5
- package/docs/interfaces/CurtainStatus.html +6 -6
- package/docs/interfaces/DeviceInfo.html +23 -13
- package/docs/interfaces/DeviceListResponse.html +2 -2
- package/docs/interfaces/DeviceStatus.html +6 -6
- package/docs/interfaces/DiscoveryOptions.html +7 -7
- package/docs/interfaces/HubServiceData.html +22 -5
- package/docs/interfaces/HubStatus.html +6 -6
- package/docs/interfaces/HumidifierCommands.html +2 -2
- package/docs/interfaces/HumidifierServiceData.html +23 -6
- package/docs/interfaces/HumidifierStatus.html +6 -6
- package/docs/interfaces/KeypadStatus.html +6 -6
- package/docs/interfaces/LeakServiceData.html +22 -5
- package/docs/interfaces/LeakStatus.html +6 -6
- package/docs/interfaces/LockCommands.html +6 -2
- package/docs/interfaces/LockServiceData.html +20 -6
- package/docs/interfaces/LockStatus.html +6 -6
- package/docs/interfaces/MeterServiceData.html +22 -5
- package/docs/interfaces/MeterStatus.html +6 -6
- package/docs/interfaces/MotionServiceData.html +22 -5
- package/docs/interfaces/MotionStatus.html +6 -6
- package/docs/interfaces/PlugCommands.html +2 -2
- package/docs/interfaces/PlugServiceData.html +21 -5
- package/docs/interfaces/PlugStatus.html +6 -6
- package/docs/interfaces/PresenceServiceData.html +22 -5
- package/docs/interfaces/PresenceStatus.html +6 -6
- package/docs/interfaces/RelaySwitchCommands.html +2 -2
- package/docs/interfaces/RelaySwitchServiceData.html +21 -5
- package/docs/interfaces/RelaySwitchStatus.html +6 -6
- package/docs/interfaces/RemoteStatus.html +6 -6
- package/docs/interfaces/SceneListResponse.html +2 -2
- package/docs/interfaces/StripCommands.html +4 -2
- package/docs/interfaces/StripServiceData.html +21 -5
- package/docs/interfaces/StripStatus.html +6 -6
- package/docs/interfaces/SwitchBotConfig.html +21 -9
- package/docs/interfaces/WebhookConfig.html +2 -2
- package/docs/interfaces/WebhookDetails.html +2 -2
- package/docs/interfaces/WebhookQueryResponse.html +2 -2
- package/docs/interfaces/WebhookSetupResponse.html +2 -2
- package/docs/media/BLE.md +117 -4
- package/docs/modules.html +1 -1
- package/docs/types/ConnectionType.html +1 -1
- package/docs/types/PhysicalDeviceType.html +1 -1
- package/docs/types/VirtualDeviceType.html +1 -1
- package/docs/variables/urls.html +1 -1
- package/package.json +11 -7
- package/tmp-switchbot-scan.mjs +79 -0
- package/todo/PYSWITCHBOT_COMPARISON.md +484 -0
- package/todo/README.md +68 -0
- package/todo/completed.md +134 -0
- package/todo/todo.md +296 -0
- package/tsconfig.build.json +17 -0
- package/PRODUCTION_READY.md +0 -135
package/dist/ble.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
*
|
|
3
3
|
* ble.ts: SwitchBot v4.0.0 - BLE Discovery and Communication
|
|
4
4
|
*/
|
|
5
|
+
import { Buffer } from 'node:buffer';
|
|
6
|
+
import { createCipheriv } from 'node:crypto';
|
|
5
7
|
import { EventEmitter } from 'node:events';
|
|
6
|
-
import { BLENotAvailableError, DeviceNotFoundError } from './errors.js';
|
|
8
|
+
import { BLENotAvailableError, CommandFailedError, DeviceNotFoundError } from './errors.js';
|
|
7
9
|
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';
|
|
8
|
-
import { Logger, normalizeMAC, withTimeout } from './utils/index.js';
|
|
10
|
+
import { Logger, macToDeviceId, normalizeMAC, withTimeout, extractMacFromManufacturerData } from './utils/index.js';
|
|
9
11
|
/**
|
|
10
12
|
* BLE Scanner for discovering SwitchBot devices
|
|
11
13
|
*/
|
|
@@ -14,6 +16,11 @@ export class BLEScanner extends EventEmitter {
|
|
|
14
16
|
logger;
|
|
15
17
|
scanning = false;
|
|
16
18
|
discoveredDevices = new Map();
|
|
19
|
+
discoveredModelCache = new Map();
|
|
20
|
+
nobleStateHandler;
|
|
21
|
+
nobleDiscoverHandler;
|
|
22
|
+
nobleScanStartHandler;
|
|
23
|
+
nobleScanStopHandler;
|
|
17
24
|
noblePromise = null;
|
|
18
25
|
constructor(options = {}) {
|
|
19
26
|
super();
|
|
@@ -32,20 +39,23 @@ export class BLEScanner extends EventEmitter {
|
|
|
32
39
|
* Initialize Noble lazily
|
|
33
40
|
*/
|
|
34
41
|
async initializeNoble() {
|
|
35
|
-
if (this.noble
|
|
42
|
+
if (this.noble) {
|
|
36
43
|
return;
|
|
37
44
|
}
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return module.default;
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
throw new BLENotAvailableError('BLE not supported on this platform or @stoprocent/noble not installed');
|
|
46
|
-
}
|
|
47
|
-
})();
|
|
45
|
+
if (this.noblePromise) {
|
|
46
|
+
// Wait for existing initialization to complete
|
|
47
|
+
this.noble = await this.noblePromise;
|
|
48
|
+
return;
|
|
48
49
|
}
|
|
50
|
+
this.noblePromise = (async () => {
|
|
51
|
+
try {
|
|
52
|
+
const module = await import('@stoprocent/noble');
|
|
53
|
+
return module.default;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
throw new BLENotAvailableError('BLE not supported on this platform or @stoprocent/noble not installed');
|
|
57
|
+
}
|
|
58
|
+
})();
|
|
49
59
|
try {
|
|
50
60
|
this.noble = await this.noblePromise;
|
|
51
61
|
this.setupNobleHandlers();
|
|
@@ -61,61 +71,133 @@ export class BLEScanner extends EventEmitter {
|
|
|
61
71
|
if (!this.noble) {
|
|
62
72
|
await this.initializeNoble();
|
|
63
73
|
}
|
|
74
|
+
if (!this.noble) {
|
|
75
|
+
throw new BLENotAvailableError('BLE not available - noble failed to initialize');
|
|
76
|
+
}
|
|
64
77
|
}
|
|
65
78
|
/**
|
|
66
79
|
* Setup Noble event handlers
|
|
67
80
|
*/
|
|
68
81
|
setupNobleHandlers() {
|
|
69
|
-
|
|
82
|
+
// Prevent duplicate listeners if handlers are re-initialized
|
|
83
|
+
this.removeNobleHandlers();
|
|
84
|
+
// Store handlers as properties for later cleanup
|
|
85
|
+
this.nobleStateHandler = (state) => {
|
|
70
86
|
this.logger.debug('Noble state changed:', state);
|
|
71
87
|
this.emit('state-change', state);
|
|
72
88
|
if (state === 'poweredOn') {
|
|
73
89
|
this.emit('ready');
|
|
74
90
|
}
|
|
75
|
-
}
|
|
76
|
-
this.
|
|
91
|
+
};
|
|
92
|
+
this.nobleDiscoverHandler = (peripheral) => {
|
|
77
93
|
this.handleDiscovery(peripheral);
|
|
78
|
-
}
|
|
79
|
-
this.
|
|
94
|
+
};
|
|
95
|
+
this.nobleScanStartHandler = () => {
|
|
80
96
|
this.scanning = true;
|
|
81
97
|
this.logger.info('BLE scan started');
|
|
82
98
|
this.emit('scan-start');
|
|
83
|
-
}
|
|
84
|
-
this.
|
|
99
|
+
};
|
|
100
|
+
this.nobleScanStopHandler = () => {
|
|
85
101
|
this.scanning = false;
|
|
86
102
|
this.logger.info('BLE scan stopped');
|
|
87
103
|
this.emit('scan-stop');
|
|
88
|
-
}
|
|
104
|
+
};
|
|
105
|
+
this.noble.on('stateChange', this.nobleStateHandler);
|
|
106
|
+
this.noble.on('discover', this.nobleDiscoverHandler);
|
|
107
|
+
this.noble.on('scanStart', this.nobleScanStartHandler);
|
|
108
|
+
this.noble.on('scanStop', this.nobleScanStopHandler);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Remove Noble event handlers
|
|
112
|
+
*/
|
|
113
|
+
removeNobleHandlers() {
|
|
114
|
+
if (this.nobleStateHandler) {
|
|
115
|
+
this.noble.off('stateChange', this.nobleStateHandler);
|
|
116
|
+
}
|
|
117
|
+
if (this.nobleDiscoverHandler) {
|
|
118
|
+
this.noble.off('discover', this.nobleDiscoverHandler);
|
|
119
|
+
}
|
|
120
|
+
if (this.nobleScanStartHandler) {
|
|
121
|
+
this.noble.off('scanStart', this.nobleScanStartHandler);
|
|
122
|
+
}
|
|
123
|
+
if (this.nobleScanStopHandler) {
|
|
124
|
+
this.noble.off('scanStop', this.nobleScanStopHandler);
|
|
125
|
+
}
|
|
126
|
+
this.nobleStateHandler = undefined;
|
|
127
|
+
this.nobleDiscoverHandler = undefined;
|
|
128
|
+
this.nobleScanStartHandler = undefined;
|
|
129
|
+
this.nobleScanStopHandler = undefined;
|
|
89
130
|
}
|
|
90
131
|
/**
|
|
91
132
|
* Handle device discovery
|
|
92
133
|
*/
|
|
93
134
|
handleDiscovery(peripheral) {
|
|
94
135
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
136
|
+
// Validate peripheral has required properties
|
|
137
|
+
if (!peripheral || typeof peripheral !== 'object') {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const { advertisement, address, rssi, connectable } = peripheral;
|
|
141
|
+
// Skip non-connectable devices
|
|
142
|
+
if (connectable === false) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Skip devices with invalid RSSI (typical range: -120 to 0 dBm)
|
|
146
|
+
if (typeof rssi !== 'number' || rssi < -120 || rssi > 0) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Validate advertisement object exists and has service data
|
|
150
|
+
if (!advertisement || typeof advertisement !== 'object') {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (!Array.isArray(advertisement.serviceData) || advertisement.serviceData.length === 0) {
|
|
98
154
|
return;
|
|
99
155
|
}
|
|
100
156
|
for (const serviceDataItem of advertisement.serviceData) {
|
|
101
|
-
//
|
|
102
|
-
if (serviceDataItem
|
|
157
|
+
// Validate service data item has required properties
|
|
158
|
+
if (!serviceDataItem || typeof serviceDataItem !== 'object') {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
// SwitchBot service UUID (current: fd3d, legacy: 000d)
|
|
162
|
+
const uuid = typeof serviceDataItem.uuid === 'string' ? serviceDataItem.uuid.toLowerCase() : '';
|
|
163
|
+
const isSwitchBotUUID = uuid === 'fd3d' ||
|
|
164
|
+
uuid === '0000fd3d-0000-1000-8000-00805f9b34fb' ||
|
|
165
|
+
uuid === '000d' ||
|
|
166
|
+
uuid === '0000000d-0000-1000-8000-00805f9b34fb';
|
|
167
|
+
if (!isSwitchBotUUID) {
|
|
103
168
|
continue;
|
|
104
169
|
}
|
|
105
170
|
const serviceData = this.parseServiceData(serviceDataItem.data);
|
|
106
171
|
if (!serviceData) {
|
|
107
172
|
continue;
|
|
108
173
|
}
|
|
109
|
-
|
|
174
|
+
let normalizedAddress = typeof address === 'string' && address.length > 0 ? normalizeMAC(address) : undefined;
|
|
175
|
+
// Fallback to manufacturer data MAC if service data address is empty
|
|
176
|
+
if (!normalizedAddress) {
|
|
177
|
+
const manufacturerMac = extractMacFromManufacturerData(peripheral.advertisement?.manufacturerData);
|
|
178
|
+
if (manufacturerMac) {
|
|
179
|
+
normalizedAddress = manufacturerMac;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const fallbackId = typeof peripheral.id === 'string' && peripheral.id.length > 0
|
|
183
|
+
? peripheral.id
|
|
184
|
+
: (normalizedAddress ? macToDeviceId(normalizedAddress) : undefined);
|
|
185
|
+
if (!fallbackId) {
|
|
186
|
+
this.logger.debug('Skipping BLE discovery with no address and no peripheral id');
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
110
189
|
const advertisement = {
|
|
111
|
-
id:
|
|
112
|
-
address:
|
|
190
|
+
id: fallbackId,
|
|
191
|
+
address: normalizedAddress,
|
|
192
|
+
isAddressable: normalizedAddress !== undefined,
|
|
113
193
|
rssi,
|
|
114
194
|
serviceData,
|
|
115
195
|
};
|
|
116
|
-
|
|
196
|
+
const discoveryKey = normalizedAddress ?? `id:${fallbackId}`;
|
|
197
|
+
this.discoveredDevices.set(discoveryKey, advertisement);
|
|
198
|
+
this.discoveredModelCache.set(discoveryKey, serviceData.model);
|
|
117
199
|
this.emit('discover', advertisement);
|
|
118
|
-
this.logger.debug(`Discovered ${serviceData.modelName} at ${
|
|
200
|
+
this.logger.debug(`Discovered ${serviceData.modelName} at ${normalizedAddress ?? fallbackId}`);
|
|
119
201
|
}
|
|
120
202
|
}
|
|
121
203
|
catch (error) {
|
|
@@ -146,6 +228,45 @@ export class BLEScanner extends EventEmitter {
|
|
|
146
228
|
if (data.length > 2) {
|
|
147
229
|
serviceData.battery = data[2] & 0x7F;
|
|
148
230
|
}
|
|
231
|
+
// Model-specific advertisement parsing for status without active connection
|
|
232
|
+
if (model === 'H' && data.length > 1) {
|
|
233
|
+
// Bot (WoHand): infer mode/state from status bits
|
|
234
|
+
serviceData.mode = (data[1] & 0x80) ? 'switch' : 'press';
|
|
235
|
+
serviceData.state = (data[1] & 0x40) !== 0;
|
|
236
|
+
}
|
|
237
|
+
if ((model === 'c' || model === '{') && data.length > 4) {
|
|
238
|
+
// Curtain/Curtain3
|
|
239
|
+
serviceData.inMotion = (data[1] & 0x40) !== 0;
|
|
240
|
+
serviceData.position = Math.min(100, Math.max(0, data[3] & 0x7F));
|
|
241
|
+
serviceData.lightLevel = data[4] & 0x7F;
|
|
242
|
+
serviceData.calibration = (data[1] & 0x20) !== 0;
|
|
243
|
+
if (data.length > 5) {
|
|
244
|
+
serviceData.deviceChain = data[5] & 0x03;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if ((model === 'o' || model === '\x11') && data.length > 1) {
|
|
248
|
+
// Lock / Lock Pro
|
|
249
|
+
const lockStatus = data[1] & 0x0F;
|
|
250
|
+
serviceData.status = lockStatus;
|
|
251
|
+
serviceData.doorOpen = (data[1] & 0x10) !== 0;
|
|
252
|
+
serviceData.calibration = (data[1] & 0x80) !== 0;
|
|
253
|
+
serviceData.lockState = lockStatus === 1
|
|
254
|
+
? 'unlocked'
|
|
255
|
+
: lockStatus === 2
|
|
256
|
+
? 'jammed'
|
|
257
|
+
: 'locked';
|
|
258
|
+
if (data.length > 3) {
|
|
259
|
+
serviceData.sequenceNumber = data[3];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if ((model === '\x0D' || model === '\x0E') && data.length > 1) {
|
|
263
|
+
// Relay Switch 1PM / Relay Switch 1
|
|
264
|
+
serviceData.state = (data[1] & 0x01) === 0x01;
|
|
265
|
+
serviceData.channel2State = (data[1] & 0x02) === 0x02;
|
|
266
|
+
if (data.length > 3) {
|
|
267
|
+
serviceData.sequenceNumber = data[3];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
149
270
|
return serviceData;
|
|
150
271
|
}
|
|
151
272
|
catch (error) {
|
|
@@ -160,6 +281,9 @@ export class BLEScanner extends EventEmitter {
|
|
|
160
281
|
await this.ensureNoble();
|
|
161
282
|
const { duration = BLE_SCAN_TIMEOUT, active = true } = options;
|
|
162
283
|
this.logger.info('Starting BLE scan', { duration, active });
|
|
284
|
+
if (!this.noble) {
|
|
285
|
+
throw new BLENotAvailableError('BLE not available - noble failed to initialize');
|
|
286
|
+
}
|
|
163
287
|
if (this.noble.state !== 'poweredOn') {
|
|
164
288
|
await new Promise((resolve, reject) => {
|
|
165
289
|
const timeout = setTimeout(() => {
|
|
@@ -173,6 +297,7 @@ export class BLEScanner extends EventEmitter {
|
|
|
173
297
|
}
|
|
174
298
|
// Clear previous discoveries if starting fresh
|
|
175
299
|
this.discoveredDevices.clear();
|
|
300
|
+
this.discoveredModelCache.clear();
|
|
176
301
|
// Start scanning
|
|
177
302
|
this.noble.startScanning([], active);
|
|
178
303
|
// Auto-stop after duration
|
|
@@ -192,6 +317,15 @@ export class BLEScanner extends EventEmitter {
|
|
|
192
317
|
this.noble.stopScanning();
|
|
193
318
|
}
|
|
194
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Cleanup all resources
|
|
322
|
+
*/
|
|
323
|
+
destroy() {
|
|
324
|
+
this.stopScan();
|
|
325
|
+
this.removeNobleHandlers();
|
|
326
|
+
this.discoveredDevices.clear();
|
|
327
|
+
this.discoveredModelCache.clear();
|
|
328
|
+
}
|
|
195
329
|
/**
|
|
196
330
|
* Get all discovered devices
|
|
197
331
|
*/
|
|
@@ -199,10 +333,19 @@ export class BLEScanner extends EventEmitter {
|
|
|
199
333
|
return Array.from(this.discoveredDevices.values());
|
|
200
334
|
}
|
|
201
335
|
/**
|
|
202
|
-
* Get discovered device by MAC
|
|
336
|
+
* Get discovered device by MAC or BLE ID
|
|
203
337
|
*/
|
|
204
|
-
getDevice(mac) {
|
|
205
|
-
|
|
338
|
+
getDevice(mac, bleId) {
|
|
339
|
+
// Try MAC lookup first
|
|
340
|
+
const byMac = this.discoveredDevices.get(normalizeMAC(mac));
|
|
341
|
+
if (byMac) {
|
|
342
|
+
return byMac;
|
|
343
|
+
}
|
|
344
|
+
// Fall back to ID-based lookup
|
|
345
|
+
if (bleId) {
|
|
346
|
+
return this.discoveredDevices.get(`id:${bleId}`);
|
|
347
|
+
}
|
|
348
|
+
return undefined;
|
|
206
349
|
}
|
|
207
350
|
/**
|
|
208
351
|
* Check if currently scanning
|
|
@@ -213,17 +356,20 @@ export class BLEScanner extends EventEmitter {
|
|
|
213
356
|
/**
|
|
214
357
|
* Wait for specific device
|
|
215
358
|
*/
|
|
216
|
-
async waitForDevice(mac, timeoutMs = BLE_SCAN_TIMEOUT) {
|
|
359
|
+
async waitForDevice(mac, timeoutMs = BLE_SCAN_TIMEOUT, bleId) {
|
|
217
360
|
const normalizedMac = normalizeMAC(mac);
|
|
218
|
-
// Check if already discovered
|
|
219
|
-
const existing = this.discoveredDevices.get(normalizedMac);
|
|
361
|
+
// Check if already discovered (try MAC first, then ID)
|
|
362
|
+
const existing = this.discoveredDevices.get(normalizedMac) || (bleId ? this.discoveredDevices.get(`id:${bleId}`) : undefined);
|
|
220
363
|
if (existing) {
|
|
221
364
|
return existing;
|
|
222
365
|
}
|
|
223
366
|
// Wait for discovery
|
|
224
367
|
return withTimeout(new Promise((resolve) => {
|
|
225
368
|
const handler = (advertisement) => {
|
|
226
|
-
|
|
369
|
+
// Match by address (if available) or by ID
|
|
370
|
+
const matches = (advertisement.address && normalizeMAC(advertisement.address) === normalizedMac) ||
|
|
371
|
+
(bleId && advertisement.id === bleId);
|
|
372
|
+
if (matches) {
|
|
227
373
|
this.off('discover', handler);
|
|
228
374
|
resolve(advertisement);
|
|
229
375
|
}
|
|
@@ -240,6 +386,11 @@ export class BLEConnection {
|
|
|
240
386
|
logger;
|
|
241
387
|
connections = new Map(); // Map of MAC -> Peripheral
|
|
242
388
|
characteristics = new Map();
|
|
389
|
+
disconnectTimers = new Map();
|
|
390
|
+
operationLocks = new Map();
|
|
391
|
+
encryptionConfig = new Map();
|
|
392
|
+
notificationHandlers = new Map();
|
|
393
|
+
persistentConnectionMs = 8500;
|
|
243
394
|
noblePromise = null;
|
|
244
395
|
constructor(options = {}) {
|
|
245
396
|
this.logger = new Logger('BLEConnection', options.logLevel);
|
|
@@ -254,20 +405,23 @@ export class BLEConnection {
|
|
|
254
405
|
* Initialize Noble lazily
|
|
255
406
|
*/
|
|
256
407
|
async initializeNoble() {
|
|
257
|
-
if (this.noble
|
|
408
|
+
if (this.noble) {
|
|
258
409
|
return;
|
|
259
410
|
}
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
return module.default;
|
|
265
|
-
}
|
|
266
|
-
catch (error) {
|
|
267
|
-
throw new BLENotAvailableError('BLE not supported on this platform or @stoprocent/noble not installed');
|
|
268
|
-
}
|
|
269
|
-
})();
|
|
411
|
+
if (this.noblePromise) {
|
|
412
|
+
// Wait for existing initialization to complete
|
|
413
|
+
this.noble = await this.noblePromise;
|
|
414
|
+
return;
|
|
270
415
|
}
|
|
416
|
+
this.noblePromise = (async () => {
|
|
417
|
+
try {
|
|
418
|
+
const module = await import('@stoprocent/noble');
|
|
419
|
+
return module.default;
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
throw new BLENotAvailableError('BLE not supported on this platform or @stoprocent/noble not installed');
|
|
423
|
+
}
|
|
424
|
+
})();
|
|
271
425
|
try {
|
|
272
426
|
this.noble = await this.noblePromise;
|
|
273
427
|
}
|
|
@@ -282,6 +436,178 @@ export class BLEConnection {
|
|
|
282
436
|
if (!this.noble) {
|
|
283
437
|
await this.initializeNoble();
|
|
284
438
|
}
|
|
439
|
+
if (!this.noble) {
|
|
440
|
+
throw new BLENotAvailableError('BLE not available - noble failed to initialize');
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
async withMacLock(mac, fn) {
|
|
444
|
+
const normalizedMac = normalizeMAC(mac);
|
|
445
|
+
const previousLock = this.operationLocks.get(normalizedMac) ?? Promise.resolve();
|
|
446
|
+
let releaseCurrent = () => { };
|
|
447
|
+
const currentLock = new Promise((resolve) => {
|
|
448
|
+
releaseCurrent = resolve;
|
|
449
|
+
});
|
|
450
|
+
const chainedLock = previousLock.then(() => currentLock);
|
|
451
|
+
this.operationLocks.set(normalizedMac, chainedLock);
|
|
452
|
+
await previousLock;
|
|
453
|
+
try {
|
|
454
|
+
return await fn();
|
|
455
|
+
}
|
|
456
|
+
finally {
|
|
457
|
+
releaseCurrent();
|
|
458
|
+
if (this.operationLocks.get(normalizedMac) === chainedLock) {
|
|
459
|
+
this.operationLocks.delete(normalizedMac);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
clearDisconnectTimer(mac) {
|
|
464
|
+
const existingTimer = this.disconnectTimers.get(mac);
|
|
465
|
+
if (existingTimer) {
|
|
466
|
+
clearTimeout(existingTimer);
|
|
467
|
+
this.disconnectTimers.delete(mac);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
scheduleDisconnect(mac) {
|
|
471
|
+
this.clearDisconnectTimer(mac);
|
|
472
|
+
const timer = setTimeout(() => {
|
|
473
|
+
this.disconnect(mac).catch((error) => {
|
|
474
|
+
this.logger.debug(`Auto-disconnect failed for ${mac}`, error);
|
|
475
|
+
});
|
|
476
|
+
}, this.persistentConnectionMs);
|
|
477
|
+
this.disconnectTimers.set(mac, timer);
|
|
478
|
+
}
|
|
479
|
+
setPersistentConnectionTimeout(timeoutMs) {
|
|
480
|
+
this.persistentConnectionMs = Math.max(1000, timeoutMs);
|
|
481
|
+
}
|
|
482
|
+
setEncryption(mac, keyHex, ivHex, mode = 'auto') {
|
|
483
|
+
const normalizedMac = normalizeMAC(mac);
|
|
484
|
+
const key = Buffer.from(keyHex, 'hex');
|
|
485
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
486
|
+
if (key.length !== 16) {
|
|
487
|
+
throw new CommandFailedError('Invalid BLE encryption key length (expected 16 bytes)', 'ble');
|
|
488
|
+
}
|
|
489
|
+
const resolvedMode = mode === 'auto'
|
|
490
|
+
? (iv.length === 12 ? 'gcm' : 'ctr')
|
|
491
|
+
: mode;
|
|
492
|
+
const expectedIvLength = resolvedMode === 'gcm' ? 12 : 16;
|
|
493
|
+
if (iv.length !== expectedIvLength) {
|
|
494
|
+
throw new CommandFailedError(`Invalid IV length for ${resolvedMode.toUpperCase()} mode`, 'ble');
|
|
495
|
+
}
|
|
496
|
+
this.encryptionConfig.set(normalizedMac, { key, iv: Buffer.from(iv), mode: resolvedMode });
|
|
497
|
+
}
|
|
498
|
+
clearEncryption(mac) {
|
|
499
|
+
this.encryptionConfig.delete(normalizeMAC(mac));
|
|
500
|
+
}
|
|
501
|
+
incrementIv(iv) {
|
|
502
|
+
const nextIv = Buffer.from(iv);
|
|
503
|
+
for (let i = nextIv.length - 1; i >= 0; i--) {
|
|
504
|
+
if ((nextIv[i] ?? 0) === 0xFF) {
|
|
505
|
+
nextIv[i] = 0x00;
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
nextIv[i] = (nextIv[i] ?? 0) + 1;
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return nextIv;
|
|
513
|
+
}
|
|
514
|
+
encryptIfConfigured(mac, data) {
|
|
515
|
+
const normalizedMac = normalizeMAC(mac);
|
|
516
|
+
const config = this.encryptionConfig.get(normalizedMac);
|
|
517
|
+
if (!config) {
|
|
518
|
+
return data;
|
|
519
|
+
}
|
|
520
|
+
if (config.mode === 'gcm') {
|
|
521
|
+
const cipher = createCipheriv('aes-128-gcm', config.key, config.iv);
|
|
522
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
523
|
+
const tag = cipher.getAuthTag().subarray(0, 2);
|
|
524
|
+
this.encryptionConfig.set(normalizedMac, {
|
|
525
|
+
...config,
|
|
526
|
+
iv: this.incrementIv(config.iv),
|
|
527
|
+
});
|
|
528
|
+
return Buffer.concat([encrypted, tag]);
|
|
529
|
+
}
|
|
530
|
+
const cipher = createCipheriv('aes-128-ctr', config.key, config.iv);
|
|
531
|
+
return Buffer.concat([cipher.update(data), cipher.final()]);
|
|
532
|
+
}
|
|
533
|
+
validateCommandResult(response, mac) {
|
|
534
|
+
if (!response || response.length === 0) {
|
|
535
|
+
throw new CommandFailedError(`Empty BLE response from ${mac}`, 'ble');
|
|
536
|
+
}
|
|
537
|
+
if (response.includes(0x07)) {
|
|
538
|
+
throw new CommandFailedError(`BLE command rejected by ${mac}: password required`, 'ble');
|
|
539
|
+
}
|
|
540
|
+
if (response.includes(0x09)) {
|
|
541
|
+
throw new CommandFailedError(`BLE command rejected by ${mac}: password incorrect`, 'ble');
|
|
542
|
+
}
|
|
543
|
+
const acknowledged = response.some(byte => byte === 0x01 || byte === 0x05 || byte === 0x06);
|
|
544
|
+
if (!acknowledged) {
|
|
545
|
+
throw new CommandFailedError(`Unexpected BLE response from ${mac}: ${response.toString('hex')}`, 'ble');
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async sendCommand(mac, data, options = {}) {
|
|
549
|
+
const normalizedMac = normalizeMAC(mac);
|
|
550
|
+
const { expectResponse = true, validateResponse = true, responseTimeoutMs = 1200, } = options;
|
|
551
|
+
return this.withMacLock(normalizedMac, async () => {
|
|
552
|
+
const payload = this.encryptIfConfigured(normalizedMac, data);
|
|
553
|
+
await this.write(normalizedMac, payload);
|
|
554
|
+
if (!expectResponse && !validateResponse) {
|
|
555
|
+
return undefined;
|
|
556
|
+
}
|
|
557
|
+
const response = await withTimeout(this.read(normalizedMac), responseTimeoutMs, `BLE response timeout from ${normalizedMac}`);
|
|
558
|
+
if (validateResponse) {
|
|
559
|
+
this.validateCommandResult(response, normalizedMac);
|
|
560
|
+
}
|
|
561
|
+
return response;
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
async subscribeNotifications(mac, handler) {
|
|
565
|
+
const normalizedMac = normalizeMAC(mac);
|
|
566
|
+
if (!this.connections.has(normalizedMac)) {
|
|
567
|
+
await this.connect(normalizedMac);
|
|
568
|
+
}
|
|
569
|
+
const chars = this.characteristics.get(normalizedMac);
|
|
570
|
+
if (!chars?.notify) {
|
|
571
|
+
throw new CommandFailedError(`Notify characteristic not available for ${normalizedMac}`, 'ble');
|
|
572
|
+
}
|
|
573
|
+
if (!this.notificationHandlers.has(normalizedMac)) {
|
|
574
|
+
this.notificationHandlers.set(normalizedMac, new Set());
|
|
575
|
+
if (typeof chars.notify.on === 'function') {
|
|
576
|
+
chars.notify.on('data', (payload) => {
|
|
577
|
+
const handlers = this.notificationHandlers.get(normalizedMac);
|
|
578
|
+
if (!handlers) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
for (const listener of handlers) {
|
|
582
|
+
listener(payload);
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
this.notificationHandlers.get(normalizedMac).add(handler);
|
|
588
|
+
if (typeof chars.notify.subscribe === 'function') {
|
|
589
|
+
await new Promise((resolve, reject) => {
|
|
590
|
+
chars.notify.subscribe((error) => {
|
|
591
|
+
if (error) {
|
|
592
|
+
reject(error);
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
resolve();
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
unsubscribeNotifications(mac, handler) {
|
|
602
|
+
const normalizedMac = normalizeMAC(mac);
|
|
603
|
+
const handlers = this.notificationHandlers.get(normalizedMac);
|
|
604
|
+
if (!handlers) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
handlers.delete(handler);
|
|
608
|
+
if (handlers.size === 0) {
|
|
609
|
+
this.notificationHandlers.delete(normalizedMac);
|
|
610
|
+
}
|
|
285
611
|
}
|
|
286
612
|
/**
|
|
287
613
|
* Connect to a device
|
|
@@ -291,13 +617,25 @@ export class BLEConnection {
|
|
|
291
617
|
const normalizedMac = normalizeMAC(mac);
|
|
292
618
|
// Already connected?
|
|
293
619
|
if (this.connections.has(normalizedMac)) {
|
|
620
|
+
this.clearDisconnectTimer(normalizedMac);
|
|
294
621
|
this.logger.debug(`Already connected to ${mac}`);
|
|
295
622
|
return;
|
|
296
623
|
}
|
|
297
624
|
this.logger.info(`Connecting to ${mac}`);
|
|
298
|
-
// Find peripheral
|
|
625
|
+
// Find peripheral (by address or ID)
|
|
299
626
|
const peripherals = await this.noble.peripherals || [];
|
|
300
|
-
|
|
627
|
+
let peripheral;
|
|
628
|
+
// Try to find by normalized MAC first
|
|
629
|
+
if (mac.startsWith('id:')) {
|
|
630
|
+
// ID-based lookup: extract the ID and find by peripheral.id
|
|
631
|
+
const bleId = mac.substring(3);
|
|
632
|
+
// Look through peripherals to find matching ID
|
|
633
|
+
peripheral = peripherals.find((p) => p.id === bleId);
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
// MAC-based lookup
|
|
637
|
+
peripheral = peripherals.find((p) => p.address && normalizeMAC(p.address) === normalizedMac);
|
|
638
|
+
}
|
|
301
639
|
if (!peripheral) {
|
|
302
640
|
throw new DeviceNotFoundError(mac);
|
|
303
641
|
}
|
|
@@ -313,9 +651,22 @@ export class BLEConnection {
|
|
|
313
651
|
});
|
|
314
652
|
}), BLE_CONNECT_TIMEOUT, `Connection to ${mac} timed out`);
|
|
315
653
|
this.connections.set(normalizedMac, peripheral);
|
|
654
|
+
this.clearDisconnectTimer(normalizedMac);
|
|
316
655
|
this.logger.info(`Connected to ${mac}`);
|
|
317
|
-
// Discover characteristics
|
|
318
|
-
|
|
656
|
+
// Discover characteristics (may throw)
|
|
657
|
+
try {
|
|
658
|
+
await this.discoverCharacteristics(normalizedMac, peripheral);
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
// Clean up partial connection if characteristic discovery fails
|
|
662
|
+
this.connections.delete(normalizedMac);
|
|
663
|
+
this.characteristics.delete(normalizedMac);
|
|
664
|
+
// Best effort disconnect to avoid leaked active BLE links
|
|
665
|
+
await new Promise((resolve) => {
|
|
666
|
+
peripheral.disconnect(() => resolve());
|
|
667
|
+
});
|
|
668
|
+
throw error;
|
|
669
|
+
}
|
|
319
670
|
}
|
|
320
671
|
/**
|
|
321
672
|
* Discover service characteristics
|
|
@@ -345,6 +696,9 @@ export class BLEConnection {
|
|
|
345
696
|
async disconnect(mac) {
|
|
346
697
|
const normalizedMac = normalizeMAC(mac);
|
|
347
698
|
const peripheral = this.connections.get(normalizedMac);
|
|
699
|
+
this.clearDisconnectTimer(normalizedMac);
|
|
700
|
+
this.clearEncryption(normalizedMac);
|
|
701
|
+
this.notificationHandlers.delete(normalizedMac);
|
|
348
702
|
if (!peripheral) {
|
|
349
703
|
return;
|
|
350
704
|
}
|
|
@@ -377,6 +731,7 @@ export class BLEConnection {
|
|
|
377
731
|
reject(error);
|
|
378
732
|
}
|
|
379
733
|
else {
|
|
734
|
+
this.scheduleDisconnect(normalizedMac);
|
|
380
735
|
resolve();
|
|
381
736
|
}
|
|
382
737
|
});
|
|
@@ -402,6 +757,7 @@ export class BLEConnection {
|
|
|
402
757
|
reject(error);
|
|
403
758
|
}
|
|
404
759
|
else {
|
|
760
|
+
this.scheduleDisconnect(normalizedMac);
|
|
405
761
|
resolve(data);
|
|
406
762
|
}
|
|
407
763
|
});
|