@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.
@@ -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 list = await this.switchBotBLE.discover({ model: (dev as any).bleModel, id })
195
- if (!Array.isArray(list) || list.length === 0) {
196
- throw new Error('BLE device not found')
197
- }
198
- const deviceInst: any = list[0]
199
- if (typeof deviceInst[methodName] !== 'function') {
200
- throw new TypeError(`BLE method ${methodName} not available on device`)
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
- return await deviceInst[methodName](...args)
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
- // Try to parse BLE advertisement/serviceData first (works without cloud)
413
+ // First try model-specific / normalized parsing of BLE advertisement
373
414
  try {
374
- if (serviceData) {
415
+ const parsed = this.parseAdvertisementForDevice(dev, serviceData)
416
+ if (parsed) {
375
417
  // Power
376
- const power = serviceData.power ?? serviceData.on ?? serviceData.p
377
- if (power !== undefined) {
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
- const brightness = serviceData.brightness ?? serviceData.b
384
- if (brightness !== undefined) {
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 (r:g:b or hex or hex without #)
390
- const color = serviceData.color ?? serviceData.rgb ?? serviceData.c
391
- if (color !== undefined) {
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 serviceData was present, prefer it and return early
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