@switchbot/homebridge-switchbot 5.0.0-beta.4 → 5.0.0-beta.6
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/dist/platform-hap.d.ts.map +1 -1
- package/dist/platform-hap.js +13 -1
- package/dist/platform-hap.js.map +1 -1
- package/dist/platform-matter.d.ts +5 -0
- package/dist/platform-matter.d.ts.map +1 -1
- package/dist/platform-matter.js +110 -40
- package/dist/platform-matter.js.map +1 -1
- package/docs/variables/default.html +1 -1
- package/package.json +1 -1
- package/src/platform-hap.ts +12 -1
- package/src/platform-matter.ts +120 -38
package/src/platform-matter.ts
CHANGED
|
@@ -187,19 +187,35 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
|
|
|
187
187
|
|
|
188
188
|
// Helper to send a BLE action if Platform BLE is enabled and switchBotBLE exists
|
|
189
189
|
const sendBLE = async (methodName: string, ...args: any[]) => {
|
|
190
|
+
// Provide a small retry loop for flaky BLE operations
|
|
190
191
|
if (!this.switchBotBLE) {
|
|
191
192
|
throw new Error('Platform BLE not available')
|
|
192
193
|
}
|
|
193
194
|
const id = formatDeviceIdAsMac(dev.deviceId)
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
195
|
+
const maxRetries = (this.config.options as any)?.bleRetries ?? 2
|
|
196
|
+
const retryDelay = (this.config.options as any)?.bleRetryDelay ?? 500
|
|
197
|
+
let attempt = 0
|
|
198
|
+
while (attempt < maxRetries) {
|
|
199
|
+
try {
|
|
200
|
+
const list = await this.switchBotBLE.discover({ model: (dev as any).bleModel, id })
|
|
201
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
202
|
+
throw new Error('BLE device not found')
|
|
203
|
+
}
|
|
204
|
+
const deviceInst: any = list[0]
|
|
205
|
+
if (typeof deviceInst[methodName] !== 'function') {
|
|
206
|
+
throw new TypeError(`BLE method ${methodName} not available on device`)
|
|
207
|
+
}
|
|
208
|
+
return await deviceInst[methodName](...args)
|
|
209
|
+
} catch (e: any) {
|
|
210
|
+
attempt++
|
|
211
|
+
if (attempt >= maxRetries) {
|
|
212
|
+
throw e
|
|
213
|
+
}
|
|
214
|
+
this.log.debug(`BLE ${methodName} attempt ${attempt} failed for ${dev.deviceId}: ${e?.message ?? e}, retrying in ${retryDelay}ms`)
|
|
215
|
+
await sleep(retryDelay)
|
|
216
|
+
}
|
|
201
217
|
}
|
|
202
|
-
|
|
218
|
+
throw new Error('BLE operation failed')
|
|
203
219
|
}
|
|
204
220
|
|
|
205
221
|
const makeOnOffHandlers = (uuid: string) => ({
|
|
@@ -233,14 +249,19 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
|
|
|
233
249
|
|
|
234
250
|
// Mapping from SwitchBot deviceType -> constructor
|
|
235
251
|
const mapping: { [key: string]: any } = {
|
|
252
|
+
// Plugs / Outlets
|
|
236
253
|
'Plug': OnOffOutletAccessory,
|
|
237
254
|
'Plug Mini (US)': OnOffOutletAccessory,
|
|
238
255
|
'Plug Mini (JP)': OnOffOutletAccessory,
|
|
256
|
+
|
|
257
|
+
// Lighting
|
|
239
258
|
'Color Bulb': ColorLightAccessory,
|
|
240
259
|
'Ceiling Light': ColorTemperatureLightAccessory,
|
|
241
260
|
'Ceiling Light Pro': ColorTemperatureLightAccessory,
|
|
242
261
|
'Strip Light': ExtendedColorLightAccessory,
|
|
243
262
|
'Dimmable Light': DimmableLightAccessory,
|
|
263
|
+
|
|
264
|
+
// Robot Vacuums
|
|
244
265
|
'K10+': RoboticVacuumAccessory,
|
|
245
266
|
'K10+ Pro': RoboticVacuumAccessory,
|
|
246
267
|
'WoSweeper': RoboticVacuumAccessory,
|
|
@@ -248,8 +269,12 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
|
|
|
248
269
|
'Robot Vacuum Cleaner S1': RoboticVacuumAccessory,
|
|
249
270
|
'Robot Vacuum Cleaner S1 Plus': RoboticVacuumAccessory,
|
|
250
271
|
'Robot Vacuum Cleaner S10': RoboticVacuumAccessory,
|
|
272
|
+
|
|
273
|
+
// Locks
|
|
251
274
|
'Smart Lock': DoorLockAccessory,
|
|
252
275
|
'Smart Lock Pro': DoorLockAccessory,
|
|
276
|
+
|
|
277
|
+
// Sensors
|
|
253
278
|
'Motion Sensor': OccupancySensorAccessory,
|
|
254
279
|
'Contact Sensor': ContactSensorAccessory,
|
|
255
280
|
'Water Detector': LeakSensorAccessory,
|
|
@@ -258,14 +283,30 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
|
|
|
258
283
|
'MeterPro': TemperatureSensorAccessory,
|
|
259
284
|
'WoIOSensor': TemperatureSensorAccessory,
|
|
260
285
|
'Air Purifier PM2.5': HumiditySensorAccessory,
|
|
286
|
+
|
|
287
|
+
// Fans
|
|
261
288
|
'Battery Circulator Fan': FanAccessory,
|
|
289
|
+
|
|
290
|
+
// Curtains / Blinds
|
|
262
291
|
'Blind Tilt': VenetianBlindAccessory,
|
|
263
292
|
'Curtain': WindowBlindAccessory,
|
|
264
293
|
'Curtain3': WindowBlindAccessory,
|
|
265
294
|
'WoRollerShade': WindowBlindAccessory,
|
|
266
295
|
'Roller Shade': WindowBlindAccessory,
|
|
296
|
+
|
|
297
|
+
// Switches / Relays
|
|
267
298
|
'Relay Switch 1': OnOffSwitchAccessory,
|
|
268
299
|
'Relay Switch 1PM': OnOffSwitchAccessory,
|
|
300
|
+
|
|
301
|
+
// Misc
|
|
302
|
+
'Hub 2': undefined,
|
|
303
|
+
'Hub 3': undefined,
|
|
304
|
+
'Bot': undefined,
|
|
305
|
+
'Humidifier': undefined,
|
|
306
|
+
'Humidifier2': undefined,
|
|
307
|
+
'Air Purifier Table PM2.5': undefined,
|
|
308
|
+
'Air Purifier VOC': undefined,
|
|
309
|
+
'Air Purifier Table VOC': undefined,
|
|
269
310
|
}
|
|
270
311
|
|
|
271
312
|
const Ctor = mapping[dev.deviceType ?? '']
|
|
@@ -369,48 +410,29 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
|
|
|
369
410
|
this.bleEventHandler[mac] = async (serviceData?: any) => {
|
|
370
411
|
const uuidLocal = baseOpts.uuid
|
|
371
412
|
|
|
372
|
-
//
|
|
413
|
+
// First try model-specific / normalized parsing of BLE advertisement
|
|
373
414
|
try {
|
|
374
|
-
|
|
415
|
+
const parsed = this.parseAdvertisementForDevice(dev, serviceData)
|
|
416
|
+
if (parsed) {
|
|
375
417
|
// Power
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const on = (String(power).toLowerCase() === 'on' || Number(power) === 1)
|
|
379
|
-
await this.api.matter.updateAccessoryState(uuidLocal, this.api.matter.clusterNames.OnOff, { onOff: on })
|
|
418
|
+
if (parsed.power !== undefined) {
|
|
419
|
+
await this.api.matter.updateAccessoryState(uuidLocal, this.api.matter.clusterNames.OnOff, { onOff: Boolean(parsed.power) })
|
|
380
420
|
}
|
|
381
421
|
|
|
382
422
|
// Brightness
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const level = Math.round((Number(brightness) / 100) * 254)
|
|
423
|
+
if (parsed.brightness !== undefined) {
|
|
424
|
+
const level = Math.round((Number(parsed.brightness) / 100) * 254)
|
|
386
425
|
await this.api.matter.updateAccessoryState(uuidLocal, this.api.matter.clusterNames.LevelControl, { currentLevel: level })
|
|
387
426
|
}
|
|
388
427
|
|
|
389
|
-
// Color
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
let r = 0
|
|
393
|
-
let g = 0
|
|
394
|
-
let b = 0
|
|
395
|
-
const c = String(color)
|
|
396
|
-
if (c.includes(':')) {
|
|
397
|
-
const parts = c.split(':').map(Number)
|
|
398
|
-
;[r, g, b] = parts
|
|
399
|
-
} else if (c.startsWith('#')) {
|
|
400
|
-
const hex = c.replace('#', '')
|
|
401
|
-
r = Number.parseInt(hex.substring(0, 2), 16)
|
|
402
|
-
g = Number.parseInt(hex.substring(2, 4), 16)
|
|
403
|
-
b = Number.parseInt(hex.substring(4, 6), 16)
|
|
404
|
-
} else if (/^[0-9a-f]{6}$/i.test(c)) {
|
|
405
|
-
r = Number.parseInt(c.substring(0, 2), 16)
|
|
406
|
-
g = Number.parseInt(c.substring(2, 4), 16)
|
|
407
|
-
b = Number.parseInt(c.substring(4, 6), 16)
|
|
408
|
-
}
|
|
428
|
+
// Color
|
|
429
|
+
if (parsed.color !== undefined) {
|
|
430
|
+
const { r, g, b } = parsed.color
|
|
409
431
|
const [h, s] = rgb2hs(r, g, b)
|
|
410
432
|
await this.api.matter.updateAccessoryState(uuidLocal, this.api.matter.clusterNames.ColorControl, { currentHue: Math.round((h / 360) * 254), currentSaturation: Math.round((s / 100) * 254) })
|
|
411
433
|
}
|
|
412
434
|
|
|
413
|
-
// If
|
|
435
|
+
// If we parsed something from serviceData prefer it and return early
|
|
414
436
|
if (serviceData) {
|
|
415
437
|
return
|
|
416
438
|
}
|
|
@@ -529,6 +551,66 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
|
|
|
529
551
|
return { response: {}, statusCode: 500 }
|
|
530
552
|
}
|
|
531
553
|
|
|
554
|
+
/**
|
|
555
|
+
* Parse BLE advertisement/serviceData into normalized fields for a given device.
|
|
556
|
+
* Returns null when serviceData is falsy or parsing fails.
|
|
557
|
+
*/
|
|
558
|
+
private parseAdvertisementForDevice(dev: device, serviceData?: any) {
|
|
559
|
+
if (!serviceData) {
|
|
560
|
+
return null
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
const sd = serviceData
|
|
564
|
+
const result: any = {}
|
|
565
|
+
|
|
566
|
+
// Power/on state - supports multiple field names used by different models
|
|
567
|
+
const power = sd.power ?? sd.on ?? sd.p
|
|
568
|
+
if (power !== undefined) {
|
|
569
|
+
result.power = (String(power).toLowerCase() === 'on' || Number(power) === 1)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Brightness (0-100)
|
|
573
|
+
const brightness = sd.brightness ?? sd.b
|
|
574
|
+
if (brightness !== undefined) {
|
|
575
|
+
result.brightness = Number(brightness)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Color - could be 'r:g:b', '#rrggbb' or 'rrggbb'
|
|
579
|
+
const color = sd.color ?? sd.rgb ?? sd.c
|
|
580
|
+
if (color !== undefined) {
|
|
581
|
+
let r = 0
|
|
582
|
+
let g = 0
|
|
583
|
+
let b = 0
|
|
584
|
+
const c = String(color)
|
|
585
|
+
if (c.includes(':')) {
|
|
586
|
+
const parts = c.split(':').map(Number)
|
|
587
|
+
;[r, g, b] = parts
|
|
588
|
+
} else if (c.startsWith('#')) {
|
|
589
|
+
const hex = c.replace('#', '')
|
|
590
|
+
r = Number.parseInt(hex.substring(0, 2), 16)
|
|
591
|
+
g = Number.parseInt(hex.substring(2, 4), 16)
|
|
592
|
+
b = Number.parseInt(hex.substring(4, 6), 16)
|
|
593
|
+
} else if (/^[0-9a-f]{6}$/i.test(c)) {
|
|
594
|
+
r = Number.parseInt(c.substring(0, 2), 16)
|
|
595
|
+
g = Number.parseInt(c.substring(2, 4), 16)
|
|
596
|
+
b = Number.parseInt(c.substring(4, 6), 16)
|
|
597
|
+
}
|
|
598
|
+
result.color = { r, g, b }
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Battery (some devices use battery or batt)
|
|
602
|
+
const battery = sd.battery ?? sd.batt
|
|
603
|
+
if (battery !== undefined) {
|
|
604
|
+
result.battery = Number(battery)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return result
|
|
608
|
+
} catch (e: any) {
|
|
609
|
+
this.log.debug(`parseAdvertisementForDevice failed for ${dev.deviceId}: ${e?.message ?? e}`)
|
|
610
|
+
return null
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
532
614
|
/**
|
|
533
615
|
* Required for DynamicPlatformPlugin
|
|
534
616
|
* Called when homebridge restores cached accessories from disk at startup
|