@switchbot/homebridge-switchbot 5.0.0-beta.134 → 5.0.0-beta.136

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.
Files changed (93) hide show
  1. package/config.schema.json +0 -11
  2. package/dist/deviceFactory.d.ts +3 -1
  3. package/dist/deviceFactory.d.ts.map +1 -1
  4. package/dist/deviceFactory.js +3 -18
  5. package/dist/deviceFactory.js.map +1 -1
  6. package/dist/devices/genericDevice.d.ts +1 -1
  7. package/dist/devices/genericDevice.d.ts.map +1 -1
  8. package/dist/devices/genericDevice.js +79 -159
  9. package/dist/devices/genericDevice.js.map +1 -1
  10. package/dist/homebridge-ui/public/index.html +107 -124
  11. package/dist/homebridge-ui/public/js/api.d.ts +1 -4
  12. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  13. package/dist/homebridge-ui/public/js/api.js +1 -7
  14. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  15. package/dist/homebridge-ui/public/js/api.ts +1 -8
  16. package/dist/homebridge-ui/public/js/app.js +201 -90
  17. package/dist/homebridge-ui/public/js/app.js.map +2 -2
  18. package/dist/homebridge-ui/public/js/app.ts +5 -6
  19. package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -1
  20. package/dist/homebridge-ui/public/js/credentials.js +1 -1
  21. package/dist/homebridge-ui/public/js/credentials.js.map +1 -1
  22. package/dist/homebridge-ui/public/js/credentials.ts +7 -3
  23. package/dist/homebridge-ui/public/js/devices-delete.js +1 -1
  24. package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -1
  25. package/dist/homebridge-ui/public/js/devices-delete.ts +1 -1
  26. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
  27. package/dist/homebridge-ui/public/js/devices.js +0 -1
  28. package/dist/homebridge-ui/public/js/devices.js.map +1 -1
  29. package/dist/homebridge-ui/public/js/devices.ts +0 -2
  30. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
  31. package/dist/homebridge-ui/public/js/discovery.js +6 -3
  32. package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
  33. package/dist/homebridge-ui/public/js/discovery.ts +9 -3
  34. package/dist/homebridge-ui/public/js/logger.d.ts +1 -0
  35. package/dist/homebridge-ui/public/js/logger.d.ts.map +1 -1
  36. package/dist/homebridge-ui/public/js/logger.js +8 -3
  37. package/dist/homebridge-ui/public/js/logger.js.map +1 -1
  38. package/dist/homebridge-ui/public/js/logger.ts +12 -6
  39. package/dist/homebridge-ui/public/js/modals.d.ts +4 -0
  40. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
  41. package/dist/homebridge-ui/public/js/modals.js +152 -59
  42. package/dist/homebridge-ui/public/js/modals.js.map +1 -1
  43. package/dist/homebridge-ui/public/js/modals.ts +187 -60
  44. package/dist/homebridge-ui/public/js/render.d.ts +3 -0
  45. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
  46. package/dist/homebridge-ui/public/js/render.js +63 -24
  47. package/dist/homebridge-ui/public/js/render.js.map +1 -1
  48. package/dist/homebridge-ui/public/js/render.ts +102 -51
  49. package/dist/homebridge-ui/utils/logger.js +1 -1
  50. package/dist/homebridge-ui/utils/logger.js.map +1 -1
  51. package/dist/platform.d.ts.map +1 -1
  52. package/dist/platform.js +40 -32
  53. package/dist/platform.js.map +1 -1
  54. package/dist/switchbotClient.d.ts +5 -5
  55. package/dist/switchbotClient.d.ts.map +1 -1
  56. package/dist/switchbotClient.js +21 -12
  57. package/dist/switchbotClient.js.map +1 -1
  58. package/docs/variables/default.html +1 -1
  59. package/package.json +1 -1
  60. package/scripts/build-ui.js +4 -3
  61. package/scripts/generate-matter-maps.js +34 -17
  62. package/scripts/sync-device-types.mjs +4 -1
  63. package/src/deviceFactory.ts +3 -18
  64. package/src/devices/genericDevice.ts +77 -164
  65. package/src/homebridge-ui/public/index.html +107 -124
  66. package/src/homebridge-ui/public/js/api.ts +1 -8
  67. package/src/homebridge-ui/public/js/app.ts +5 -6
  68. package/src/homebridge-ui/public/js/credentials.ts +7 -3
  69. package/src/homebridge-ui/public/js/devices-delete.ts +1 -1
  70. package/src/homebridge-ui/public/js/devices.ts +0 -2
  71. package/src/homebridge-ui/public/js/discovery.ts +9 -3
  72. package/src/homebridge-ui/public/js/logger.ts +12 -6
  73. package/src/homebridge-ui/public/js/modals.ts +187 -60
  74. package/src/homebridge-ui/public/js/render.ts +102 -51
  75. package/src/homebridge-ui/utils/logger.ts +1 -1
  76. package/src/platform.ts +40 -31
  77. package/src/switchbotClient.ts +42 -27
  78. package/test/accessory-restore.spec.ts +9 -8
  79. package/test/device-mapping.spec.ts +4 -3
  80. package/test/deviceFactory.spec.ts +4 -3
  81. package/test/e2e/run-e2e.spec.ts +6 -10
  82. package/test/fan-swing.spec.ts +7 -6
  83. package/test/helpers/matter-harness.ts +18 -7
  84. package/test/lock-users.spec.ts +5 -5
  85. package/test/matter-childbridge.spec.ts +10 -7
  86. package/test/matter-descriptors.spec.ts +8 -8
  87. package/test/matter-device-state.spec.ts +52 -36
  88. package/test/matter-integration.spec.ts +53 -6
  89. package/test/platform.integration.spec.ts +7 -5
  90. package/test/switchbot-client-debounce.spec.ts +36 -40
  91. package/test/switchbot-client-openapi.spec.ts +22 -15
  92. package/test/switchbotClient.spec.ts +4 -3
  93. package/test/utils.spec.ts +2 -1
@@ -139,16 +139,10 @@ function classForType(type: string) {
139
139
  return DEVICE_CLASS_MAP[key] ?? GenericDevice
140
140
  }
141
141
 
142
- export async function createDevice(opts: DeviceOptions, cfg: SwitchBotPluginConfig, useMatter: boolean) {
142
+ export async function createDevice(opts: DeviceOptions, cfg: SwitchBotPluginConfig, useMatter: boolean, log?: { info: (...args: any[]) => void }) {
143
143
  // Debug: Log the options passed to the device constructor
144
- if (opts && opts.name) {
145
- if (typeof opts.matterType !== 'undefined') {
146
- // eslint-disable-next-line no-console
147
- console.info(`[Matter/Debug] createDevice: Passing opts for ${opts.name}:`, JSON.stringify(opts, null, 2))
148
- } else {
149
- // eslint-disable-next-line no-console
150
- console.info(`[Matter/Debug] createDevice: Passing opts for ${opts.name} (no matterType):`, JSON.stringify(opts, null, 2))
151
- }
144
+ if (opts && opts.name && log && typeof log.info === 'function') {
145
+ log.info(`[Matter/Debug] createDevice: Passing opts for ${opts.name}:`, JSON.stringify(opts, null, 2))
152
146
  }
153
147
  // Reuse existing client when provided via config to avoid creating
154
148
  // a new SwitchBotClient per device (which can be expensive).
@@ -169,15 +163,6 @@ export async function createDevice(opts: DeviceOptions, cfg: SwitchBotPluginConf
169
163
  deviceOpts.keyId = opts.keyId
170
164
  }
171
165
 
172
- // Ensure matterType override is set for curtain devices
173
- const typeKey = (opts.type || '').toLowerCase()
174
- if ((typeKey === 'curtain' || typeKey === 'curtain3' || typeKey === 'rollershade' || typeKey === 'blindtilt') && cfg && cfg.devices) {
175
- const configDevice = cfg.devices.find((d: any) => d.id === opts.id)
176
- if (configDevice && typeof configDevice.matterType !== 'undefined') {
177
- deviceOpts.matterType = configDevice.matterType
178
- }
179
- }
180
-
181
166
  const DeviceCtor = classForType(opts.type)
182
167
  const device = new DeviceCtor(deviceOpts, mergedCfg)
183
168
  await device.init()
@@ -7,9 +7,9 @@ import { Buffer } from 'node:buffer'
7
7
  import { MATTER_ATTRIBUTE_IDS, MATTER_CLUSTER_IDS } from '../utils.js'
8
8
  import { DeviceBase } from './deviceBase.js'
9
9
 
10
- function validateBLEResponseLength(buf: Buffer | Uint8Array | any[], expected: number, context = ''): boolean {
10
+ function validateBLEResponseLength(buf: Buffer | Uint8Array | any[], expected: number, context = '', log: import('homebridge').Logger): boolean {
11
11
  if (!buf || typeof buf.length !== 'number' || buf.length !== expected) {
12
- console.warn(`[BLE] Invalid response length${context ? ` for ${context}` : ''}: expected ${expected}, got ${buf?.length}`)
12
+ log.warn(`[BLE] Invalid response length${context ? ` for ${context}` : ''}: expected ${expected}, got ${buf?.length}`)
13
13
  return false
14
14
  }
15
15
  return true
@@ -22,12 +22,15 @@ const BLE_NOTIFICATION_HANDLERS = new Map<string, (payload: Buffer) => void>()
22
22
  const HEX_COLOR_REGEX = /^#?[0-9A-F]{6}$/i
23
23
 
24
24
  export class GenericDevice extends DeviceBase {
25
- protected log: any
25
+ protected log: import('homebridge').Logger
26
26
 
27
27
  constructor(opts: any, cfg: SwitchBotPluginConfig) {
28
28
  super(opts, cfg)
29
- // Prefer logger from opts, then cfg, fallback to console
30
- this.log = opts?.log || cfg?.log || console
29
+ // Require logger from opts or cfg
30
+ this.log = opts?.log || cfg?.log
31
+ if (!this.log) {
32
+ throw new Error('Device requires a logger (Homebridge logger) in opts or cfg')
33
+ }
31
34
  // If BLE encryptionKey/keyId provided, set on node-switchbot device instance if possible
32
35
  if (opts.encryptionKey && this.client && typeof this.client.devices?.get === 'function') {
33
36
  try {
@@ -42,10 +45,7 @@ export class GenericDevice extends DeviceBase {
42
45
  // ignore if device not found or setKey not available
43
46
  }
44
47
  }
45
- // Ensure log is always set
46
- if (!this.log) {
47
- this.log = console
48
- }
48
+ // ...existing code...
49
49
  // Subscribe to BLE notifications if supported (node-switchbot v4+)
50
50
  this._subscribeBLENotifications()
51
51
  }
@@ -115,7 +115,7 @@ export class GenericDevice extends DeviceBase {
115
115
  // If this is a BLE buffer/array, validate length (common BLE status: 12 bytes, but may vary by device)
116
116
  if (raw && (raw instanceof Buffer || Array.isArray(raw) || raw instanceof Uint8Array)) {
117
117
  // Default to 12, override per device if needed
118
- if (!validateBLEResponseLength(raw, 12, this.opts.type)) {
118
+ if (!validateBLEResponseLength(raw, 12, this.opts.type, this.log)) {
119
119
  return { id: this.opts.id, type: this.opts.type, error: 'invalid_ble_response_length', raw }
120
120
  }
121
121
  }
@@ -431,168 +431,81 @@ export class CurtainDevice extends GenericDevice {
431
431
  }
432
432
  }
433
433
 
434
- // Matter-specific descriptor for Curtain (Shade cluster) with conformance type
434
+ // Matter-specific descriptor for Curtain (WindowCovering cluster) with new attributes
435
435
  async createMatterAccessory(api: any): Promise<any> {
436
- // Determine device type and features
437
- const typeKey = (this.opts.type || '').toLowerCase()
438
- // Dynamic detection: check config/state for lift/tilt support
439
- const hasConfigLift = !!this.opts.lift || typeof this.opts.lift === 'number'
440
- const hasConfigTilt = !!this.opts.tilt || typeof this.opts.tilt === 'number'
441
- let hasLiftAttrs = false
442
- let hasTiltAttrs = false
443
- if (typeof this.getState === 'function') {
444
- let state: any
445
- try {
446
- state = await this.getState()
447
- } catch (e) {
448
- state = undefined
449
- }
450
- if (state) {
451
- hasLiftAttrs = typeof state.position === 'number'
452
- hasTiltAttrs = typeof state.tilt === 'number'
453
- }
454
- }
455
- // For SwitchBot Curtain, always set supportsLift = true
456
- let supportsLift = hasConfigLift || hasLiftAttrs
457
- if (typeKey.includes('curtain')) {
458
- supportsLift = true
459
- } else if (!supportsLift && (typeKey.includes('rollershade') || typeKey.includes('blindtilt'))) {
460
- supportsLift = true
461
- }
462
- const supportsTilt = hasConfigTilt || hasTiltAttrs || typeKey.includes('blindtilt') || typeKey.includes('blind tilt')
463
-
464
- // Debug: Log feature detection variables
465
- this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} feature detection: typeKey='${typeKey}', hasConfigLift=${hasConfigLift}, hasLiftAttrs=${hasLiftAttrs}, hasConfigTilt=${hasConfigTilt}, hasTiltAttrs=${hasTiltAttrs}, supportsLift=${supportsLift}, supportsTilt=${supportsTilt}`)
466
-
467
- // If neither lift nor tilt is supported, do not register as Matter window covering
468
- if (!supportsLift && !supportsTilt) {
469
- this.log.warn(`[Matter] Skipping Matter registration for window covering device '${this.opts.name ?? this.opts.id}' (id: ${this.opts.id}): no lift or tilt support detected.`)
470
- return undefined
436
+ // Get current state for dynamic attributes
437
+ const state = await this.getState()
438
+ // Compose attributes for Matter WindowCovering cluster
439
+ const attributes: Record<string, any> = {
440
+ currentPositionLiftPercent100ths: {
441
+ read: async () => {
442
+ const s = await this.getState()
443
+ return typeof s.position === 'number' ? Math.round(s.position * 100) : 0
444
+ },
445
+ write: undefined,
446
+ },
447
+ targetPositionLiftPercent100ths: {
448
+ read: async () => {
449
+ const s = await this.getState()
450
+ return typeof s.position === 'number' ? Math.round(s.position * 100) : 0
451
+ },
452
+ write: async (v: any) => this.setState({ position: Math.round(Number(v) / 100) }),
453
+ },
454
+ operationalStatus: {
455
+ read: async () => state.operationalStatus ?? { global: 0, lift: 0, tilt: 0 },
456
+ write: undefined,
457
+ },
458
+ endProductType: {
459
+ read: async () => state.endProductType ?? 0,
460
+ write: undefined,
461
+ },
462
+ configStatus: {
463
+ read: async () => state.configStatus ?? {
464
+ operational: true,
465
+ onlineReserved: true,
466
+ liftMovementReversed: false,
467
+ liftPositionAware: true,
468
+ tiltPositionAware: false,
469
+ liftEncoderControlled: true,
470
+ tiltEncoderControlled: false,
471
+ },
472
+ write: undefined,
473
+ },
471
474
  }
472
-
473
- // Allow user to override Matter type via config (opts.matterType)
474
- if (typeof this.opts.matterType === 'number') {
475
- const matterType = this.opts.matterType
476
- this.log.info(`[Matter/Config] Using user-specified Matter type for ${this.opts.name ?? this.opts.id}: matterType=${matterType}`)
477
- // Build attributes for Matter WindowCovering
478
- const attributes: Record<string, any> = {
479
- type: {
480
- read: async () => {
481
- this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} returning config-overridden type attribute value:`, matterType)
482
- return matterType
483
- },
484
- write: undefined,
475
+ // If tilt is supported, add tilt attributes
476
+ if (typeof state.tilt === 'number') {
477
+ attributes.currentPositionTiltPercent100ths = {
478
+ read: async () => {
479
+ const s = await this.getState()
480
+ return typeof s.tilt === 'number' ? Math.round(s.tilt * 100) : 0
485
481
  },
486
- currentPosition: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: undefined },
487
- targetPosition: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: async (v: any) => this.setState({ position: Number(v) }) },
488
- }
489
- if (supportsTilt) {
490
- attributes.currentTilt = { read: async () => { const s = await this.getState(); return typeof s.tilt === 'number' ? s.tilt : 0 }, write: undefined }
491
- attributes.targetTilt = { read: async () => { const s = await this.getState(); return typeof s.tilt === 'number' ? s.tilt : 0 }, write: async (v: any) => this.setState({ tilt: Number(v) }) }
492
- }
493
- try {
494
- this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} attributes:`, JSON.stringify(attributes, null, 2))
495
- } catch (e) {
496
- this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} attributes: [unserializable]`)
497
- }
498
- const windowCoveringCluster = {
499
- type: 'WindowCovering', // for test compatibility
500
- matterType, // actual Matter type number
501
- clusterId: MATTER_CLUSTER_IDS.WindowCovering,
502
- attributes,
482
+ write: undefined,
503
483
  }
504
- const clustersArr: any = [windowCoveringCluster]
505
- // Always set clusters.windowCovering to the WindowCovering cluster by clusterId
506
- const foundWC = clustersArr.find((c: any) => c && c.clusterId === MATTER_CLUSTER_IDS.WindowCovering)
507
- clustersArr.windowCovering = foundWC || null
508
- return {
509
- id: this.opts.id,
510
- name: this.opts.name ?? this.opts.type,
511
- protocol: 'matter',
512
- clusters: clustersArr,
484
+ attributes.targetPositionTiltPercent100ths = {
485
+ read: async () => {
486
+ const s = await this.getState()
487
+ return typeof s.tilt === 'number' ? Math.round(s.tilt * 100) : 0
488
+ },
489
+ write: async (v: any) => this.setState({ tilt: Math.round(Number(v) / 100) }),
513
490
  }
514
491
  }
515
-
516
- // Only try allowed types for each feature set (per Matter spec Table 8.2)
517
- // - supportsLift && !supportsTilt: RollerShade (0), NOT Drapery (4)
518
- // - !supportsLift && supportsTilt: BlindTilt (8)
519
- // - supportsLift && supportsTilt: RollerShade (0) (or Blind, but 0 is safe)
520
- // - supportsLift && supportsTilt && typeKey.includes('drapery'): allow Drapery (4)
521
- // - !supportsLift && !supportsTilt: skip
522
- interface MatterCombo {
523
- matterType: number
524
- supportsLift: boolean
525
- supportsTilt: boolean
526
- }
527
- const combos: MatterCombo[] = []
528
- if (supportsLift && supportsTilt) {
529
- // Both supported: allow RollerShade (0), optionally Drapery (4) if typeKey includes 'drapery'
530
- combos.push({ matterType: 0, supportsLift: true, supportsTilt: true })
531
- if (typeKey.includes('drapery')) {
532
- combos.push({ matterType: 4, supportsLift: true, supportsTilt: true })
533
- }
534
- } else if (supportsLift && !supportsTilt) {
535
- // Only lift: for SwitchBot Curtain, use Drapery (4) for conformance
536
- if (typeKey.includes('curtain')) {
537
- combos.push({ matterType: 4, supportsLift: true, supportsTilt: false })
538
- } else {
539
- combos.push({ matterType: 0, supportsLift: true, supportsTilt: false })
540
- }
541
- } else if (!supportsLift && supportsTilt) {
542
- // Only tilt: allow BlindTilt (8)
543
- combos.push({ matterType: 8, supportsLift: false, supportsTilt: true })
492
+ const windowCoveringCluster = {
493
+ type: 'WindowCovering',
494
+ clusterId: MATTER_CLUSTER_IDS.WindowCovering,
495
+ attributes,
544
496
  }
545
- let lastError
546
- for (const combo of combos) {
547
- try {
548
- this.log.info(`[Matter/Retry] Trying combo for ${this.opts.name ?? this.opts.id}: matterType=${combo.matterType}, supportsLift=${combo.supportsLift}, supportsTilt=${combo.supportsTilt}`)
549
- this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} trying combo: matterType=${combo.matterType}, supportsLift=${combo.supportsLift}, supportsTilt=${combo.supportsTilt}`)
550
- // Build attributes for Matter WindowCovering (string keys only)
551
- const attributes: Record<string, any> = {
552
- type: {
553
- read: async () => {
554
- this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} returning type attribute value:`, combo.matterType)
555
- return combo.matterType
556
- },
557
- write: undefined,
558
- },
559
- currentPosition: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: undefined },
560
- targetPosition: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: async (v: any) => this.setState({ position: Number(v) }) },
561
- }
562
- if (combo.supportsTilt) {
563
- attributes.currentTilt = { read: async () => { const s = await this.getState(); return typeof s.tilt === 'number' ? s.tilt : 0 }, write: undefined }
564
- attributes.targetTilt = { read: async () => { const s = await this.getState(); return typeof s.tilt === 'number' ? s.tilt : 0 }, write: async (v: any) => this.setState({ tilt: Number(v) }) }
565
- }
566
- // Log the final attributes object
567
- try {
568
- this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} attributes:`, JSON.stringify(attributes, null, 2))
569
- } catch (e) {
570
- this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} attributes: [unserializable]`)
571
- }
572
- // Try to register with this combo
573
- const windowCoveringCluster = {
574
- type: 'WindowCovering', // for test compatibility
575
- matterType: combo.matterType, // actual Matter type number
576
- clusterId: MATTER_CLUSTER_IDS.WindowCovering,
577
- attributes,
578
- }
579
- const clustersArr: any = [windowCoveringCluster]
580
- // Always set clusters.windowCovering to the WindowCovering cluster by clusterId
581
- const foundWC = clustersArr.find((c: any) => c && c.clusterId === MATTER_CLUSTER_IDS.WindowCovering)
582
- clustersArr.windowCovering = foundWC || null
583
- return {
584
- id: this.opts.id,
585
- name: this.opts.name ?? this.opts.type,
586
- protocol: 'matter',
587
- clusters: clustersArr,
588
- }
589
- } catch (err) {
590
- lastError = err
591
- this.log.warn(`[Matter/Retry] Failed combo matterType=${combo.matterType}, supportsLift=${combo.supportsLift}, supportsTilt=${combo.supportsTilt}: ${err}`)
592
- }
497
+ // Provide both array and named property for clusters for compatibility with test expectations
498
+ const clustersArr: any[] = [windowCoveringCluster]
499
+ const clusters: any = [...clustersArr]
500
+ // Always set clusters.windowCovering to the WindowCovering cluster by clusterId
501
+ const foundWC = clustersArr.find((c: any) => c && c.clusterId === MATTER_CLUSTER_IDS.WindowCovering)
502
+ clusters.windowCovering = foundWC || null
503
+ return {
504
+ id: this.opts.id,
505
+ name: this.opts.name ?? this.opts.type,
506
+ protocol: 'matter',
507
+ clusters,
593
508
  }
594
- // If all combos fail, throw last error
595
- throw lastError || new Error('All Matter window covering type combos failed')
596
509
  }
597
510
  }
598
511
 
@@ -1,145 +1,128 @@
1
1
  <!doctype html>
2
2
  <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width,initial-scale=1" />
6
- <title>SwitchBot Plugin UI</title>
7
- <link rel="stylesheet" href="css/styles.css" />
8
- </head>
9
- <body>
10
- <h1>🤖 SwitchBot Configuration</h1>
11
3
 
12
- <div class="card compact">
13
- <h2>API Credentials</h2>
14
- <div class="credentials-row">
15
- <div class="form-group">
16
- <label for="token">Token:</label>
17
- <input type="password" id="token" placeholder="Enter token" />
18
- <div class="status" id="tokenStatus"></div>
19
- </div>
20
- <div class="form-group">
21
- <label for="secret">Secret:</label>
22
- <input type="password" id="secret" placeholder="Enter secret" />
23
- <div class="status" id="secretStatus"></div>
24
- </div>
25
- </div>
26
- <button id="saveBtn" onclick="saveCredentials()">Save</button>
27
- <div id="saveStatus"></div>
28
- </div>
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
7
+ <title>SwitchBot Plugin UI</title>
8
+ <link rel="stylesheet" href="css/styles.css" />
9
+ </head>
29
10
 
30
- <div class="card compact">
31
- <h2>Discover Devices</h2>
32
- <div style="margin-bottom: 6px">
33
- <label style="display: flex; align-items: center; gap: 4px; cursor: pointer; font-size: 11px">
34
- <input type="checkbox" id="autoAddAllCheckbox" style="width: auto; margin: 0" />
35
- <span>Auto-add all</span>
36
- <span
37
- title="When enabled, Discover immediately adds all found devices to your config. Existing devices are skipped automatically."
38
- style="font-size: 11px; opacity: 0.7"
39
- >ⓘ</span>
40
- </label>
41
- <div style="margin-top: 2px; margin-left: 22px; font-size: 10px; opacity: 0.75; line-height: 1.25">
42
- Adds all discovered devices automatically; already configured devices are skipped.
43
- </div>
44
- </div>
45
- <div class="discovery-settings-row" style="margin-bottom: 2px">
46
- <label id="bleScanSetting" style="display: inline-flex; align-items: center; gap: 4px; font-size: 11px">
47
- <span>Scan:</span>
48
- <select id="bleScanDurationSelect" style="font-size: 11px; padding: 2px 6px">
49
- <option value="3">3s</option>
50
- <option value="5" selected>5s</option>
51
- <option value="10">10s</option>
52
- <option value="15">15s</option>
53
- </select>
54
- </label>
55
- <label id="bleTimeoutSetting" style="display: inline-flex; align-items: center; gap: 4px; font-size: 11px">
56
- <span>Timeout:</span>
57
- <input
58
- id="bleTimeoutInput"
59
- type="number"
60
- min="3"
61
- max="30"
62
- step="1"
63
- value="8"
64
- style="width: 56px; font-size: 11px; padding: 2px 4px"
65
- />
66
- <span>s</span>
67
- </label>
68
- <label style="display: flex; align-items: center; gap: 4px; cursor: pointer; font-size: 11px">
69
- <input type="checkbox" id="disableBleScanCheckbox" style="width: auto; margin: 0" />
70
- <span>Disable BLE scan</span>
71
- </label>
11
+ <body>
12
+ <h1>🤖 SwitchBot Configuration</h1>
13
+
14
+ <div class="card compact">
15
+ <h2>API Credentials</h2>
16
+ <div class="credentials-row">
17
+ <div class="form-group">
18
+ <label for="token">Token:</label>
19
+ <input type="password" id="token" placeholder="Enter token" />
20
+ <div class="status" id="tokenStatus"></div>
72
21
  </div>
73
- <div id="bluetoothStatus" style="margin-bottom: 4px; font-size: 10px; opacity: 0.8">Bluetooth: checking...</div>
74
- <div class="discovery-settings-row" style="margin-bottom: 8px">
75
- <div id="lastScannedStatus" style="font-size: 10px; opacity: 0.8; flex: 1 1 auto">Last scanned: never</div>
76
- <label style="font-size: 11px">
77
- Auto-refresh:
78
- <select id="autoRefreshIntervalSelect" style="margin-left: 4px; font-size: 11px; padding: 2px 6px">
79
- <option value="0" selected>Off</option>
80
- <option value="30">30s</option>
81
- <option value="60">1m</option>
82
- <option value="300">5m</option>
83
- </select>
84
- </label>
85
- <button id="refreshDiscoverBtn" class="secondary" style="padding: 4px 9px; font-size: 11px">Refresh</button>
22
+ <div class="form-group">
23
+ <label for="secret">Secret:</label>
24
+ <input type="password" id="secret" placeholder="Enter secret" />
25
+ <div class="status" id="secretStatus"></div>
86
26
  </div>
87
- <button id="discoverBtn" onclick="discoverDevices()" style="margin-bottom: 8px">
88
- 🔍 Discover
89
- </button>
90
- <button
91
- id="cancelDiscoverBtn"
92
- style="margin-left: 6px; margin-bottom: 8px; display: none; padding: 6px 10px; font-size: 11px"
93
- class="secondary"
94
- >
95
- Cancel
96
- </button>
97
- <div id="discoverStatus" style="margin-bottom: 6px; font-size: 11px"></div>
98
- <div id="discoverDevicesFound" style="margin-bottom: 6px; font-size: 11px; display: none"></div>
99
- <div id="discoverPhaseProgress" class="discover-phase-progress" style="display: none">
100
- <div class="discover-phase-track">
101
- <div id="discoverPhaseFill" class="discover-phase-fill"></div>
102
- </div>
103
- <div id="discoverPhaseLabel" class="discover-phase-label"></div>
27
+ </div>
28
+ <button id="saveBtn" onclick="saveCredentials()">Save</button>
29
+ <div id="saveStatus"></div>
30
+ </div>
31
+
32
+ <div class="card compact">
33
+ <h2>Discover Devices</h2>
34
+ <div style="margin-bottom: 6px">
35
+ <label style="display: flex; align-items: center; gap: 4px; cursor: pointer; font-size: 11px">
36
+ <input type="checkbox" id="autoAddAllCheckbox" style="width: auto; margin: 0" />
37
+ <span>Auto-add all</span>
38
+ <span
39
+ title="When enabled, Discover immediately adds all found devices to your config. Existing devices are skipped automatically."
40
+ style="font-size: 11px; opacity: 0.7">ⓘ</span>
41
+ </label>
42
+ <div style="margin-top: 2px; margin-left: 22px; font-size: 10px; opacity: 0.75; line-height: 1.25">
43
+ Adds all discovered devices automatically; already configured devices are skipped.
104
44
  </div>
105
- <div id="discoveredList" style="display: none">
106
- <h3 style="margin-top: 0; font-size: 12px; margin-bottom: 6px; font-weight: 600">Available Devices</h3>
107
- <ul
108
- id="discoveredDevices"
109
- style="max-height: 300px; overflow-y: auto"
110
- ></ul>
45
+ </div>
46
+ <div class="discovery-settings-row" style="margin-bottom: 2px">
47
+ <label id="bleScanSetting" style="display: inline-flex; align-items: center; gap: 4px; font-size: 11px">
48
+ <span>Scan:</span>
49
+ <select id="bleScanDurationSelect" style="font-size: 11px; padding: 2px 6px">
50
+ <option value="3">3s</option>
51
+ <option value="5" selected>5s</option>
52
+ <option value="10">10s</option>
53
+ <option value="15">15s</option>
54
+ </select>
55
+ </label>
56
+ <label id="bleTimeoutSetting" style="display: inline-flex; align-items: center; gap: 4px; font-size: 11px">
57
+ <span>Timeout:</span>
58
+ <input id="bleTimeoutInput" type="number" min="3" max="30" step="1" value="8"
59
+ style="width: 56px; font-size: 11px; padding: 2px 4px" />
60
+ <span>s</span>
61
+ </label>
62
+ <label style="display: flex; align-items: center; gap: 4px; cursor: pointer; font-size: 11px">
63
+ <input type="checkbox" id="disableBleScanCheckbox" style="width: auto; margin: 0" />
64
+ <span>Disable BLE scan</span>
65
+ </label>
66
+ </div>
67
+ <div id="bluetoothStatus" style="margin-bottom: 4px; font-size: 10px; opacity: 0.8">Bluetooth: checking...</div>
68
+ <div class="discovery-settings-row" style="margin-bottom: 8px">
69
+ <div id="lastScannedStatus" style="font-size: 10px; opacity: 0.8; flex: 1 1 auto">Last scanned: never</div>
70
+ <label style="font-size: 11px">
71
+ Auto-refresh:
72
+ <select id="autoRefreshIntervalSelect" style="margin-left: 4px; font-size: 11px; padding: 2px 6px">
73
+ <option value="0" selected>Off</option>
74
+ <option value="30">30s</option>
75
+ <option value="60">1m</option>
76
+ <option value="300">5m</option>
77
+ </select>
78
+ </label>
79
+ <button id="refreshDiscoverBtn" class="secondary" style="padding: 4px 9px; font-size: 11px">Refresh</button>
80
+ </div>
81
+ <button id="discoverBtn" onclick="discoverDevices()" style="margin-bottom: 8px">
82
+ 🔍 Discover
83
+ </button>
84
+ <button id="cancelDiscoverBtn"
85
+ style="margin-left: 6px; margin-bottom: 8px; display: none; padding: 6px 10px; font-size: 11px" class="secondary">
86
+ Cancel
87
+ </button>
88
+ <div id="discoverStatus" style="margin-bottom: 6px; font-size: 11px"></div>
89
+ <div id="discoverDevicesFound" style="margin-bottom: 6px; font-size: 11px; display: none"></div>
90
+ <div id="discoverPhaseProgress" class="discover-phase-progress" style="display: none">
91
+ <div class="discover-phase-track">
92
+ <div id="discoverPhaseFill" class="discover-phase-fill"></div>
111
93
  </div>
94
+ <div id="discoverPhaseLabel" class="discover-phase-label"></div>
95
+ </div>
96
+ <div id="discoveredList" style="display: none">
97
+ <h3 style="margin-top: 0; font-size: 12px; margin-bottom: 6px; font-weight: 600">Available Devices</h3>
98
+ <ul id="discoveredDevices" style="max-height: 300px; overflow-y: auto"></ul>
112
99
  </div>
100
+ </div>
113
101
 
114
- <div class="card compact">
115
- <h2>Configured Devices</h2>
116
- <div id="status">Loading…</div>
117
- <ul id="devices"></ul>
118
- <!-- BLE encryption key fields will be rendered dynamically per device in the UI (see app.ts/devices.js) -->
119
- <div
120
- id="removeAllContainer"
121
- style="
102
+ <div class="card compact">
103
+ <h2>Configured Devices</h2>
104
+ <div id="status">Loading…</div>
105
+ <ul id="devices"></ul>
106
+ <!-- BLE encryption key fields will be rendered dynamically per device in the UI (see app.ts/devices.js) -->
107
+ <div id="removeAllContainer" style="
122
108
  display: none;
123
109
  margin-top: 12px;
124
110
  padding-top: 10px;
125
111
  border-top: 1px solid rgba(0, 0, 0, 0.1);
126
- "
127
- >
128
- <button
129
- id="removeAllBtn"
130
- style="
112
+ ">
113
+ <button id="removeAllBtn" style="
131
114
  background: #dc2626;
132
115
  width: 100%;
133
116
  padding: 7px 12px;
134
117
  font-size: 12px;
135
118
  font-weight: 500;
136
- "
137
- >
138
- 🗑️ Remove All Devices
139
- </button>
140
- </div>
119
+ ">
120
+ 🗑️ Remove All Devices
121
+ </button>
141
122
  </div>
123
+ </div>
124
+
125
+ <script type="module" src="js/app.js"></script>
126
+ </body>
142
127
 
143
- <script type="module" src="js/app.js"></script>
144
- </body>
145
- </html>
128
+ </html>
@@ -192,7 +192,7 @@ export async function addDevice(
192
192
  deviceId: string,
193
193
  name: string,
194
194
  type: string,
195
- options?: { address?: string, model?: string, rssi?: number, encryptionKey?: string, keyId?: string, matterType?: number },
195
+ options?: { address?: string, model?: string, rssi?: number, encryptionKey?: string, keyId?: string },
196
196
  ): Promise<any> {
197
197
  const payload: any = { deviceId, name, type }
198
198
  if (options?.address) {
@@ -210,9 +210,6 @@ export async function addDevice(
210
210
  if (options?.keyId) {
211
211
  payload.keyId = options.keyId
212
212
  }
213
- if (options?.matterType !== undefined) {
214
- payload.matterType = options.matterType
215
- }
216
213
  uiLog.info('Adding device to config:', payload)
217
214
  const resp = await homebridge.request('/add-device', payload)
218
215
  uiLog.info('Add device response:', resp)
@@ -237,7 +234,6 @@ export async function updateDevice(
237
234
  deviceId: string,
238
235
  configDeviceName?: string,
239
236
  configDeviceType?: string,
240
- options?: { matterType?: number },
241
237
  ): Promise<any> {
242
238
  const params: any = { deviceId }
243
239
  if (configDeviceName) {
@@ -246,9 +242,6 @@ export async function updateDevice(
246
242
  if (configDeviceType) {
247
243
  params.configDeviceType = configDeviceType
248
244
  }
249
- if (options?.matterType !== undefined) {
250
- params.matterType = options.matterType
251
- }
252
245
  uiLog.info('[Update Device] Sending update request with params:', params)
253
246
  const resp = await homebridge.request('/update-device', params)
254
247
  uiLog.info('[Update Device] Update response:', resp)
@@ -1,17 +1,16 @@
1
1
  import { loadCredentialStatus, saveCredentials } from './credentials.js'
2
- import { loadConfiguredDevices, initRemoveAllButton } from './devices.js'
3
- // Wire up global event handlers
2
+ import { initRemoveAllButton, loadConfiguredDevices } from './devices.js'
4
3
  import { discoverDevices, initializeDiscoverySettings } from './discovery.js';
5
4
 
6
- (window as any).loadCredentialStatus = loadCredentialStatus
7
- ;(window as any).saveCredentials = saveCredentials
8
- ;(window as any).discoverDevices = discoverDevices
5
+ (window as any).loadCredentialStatus = loadCredentialStatus;
6
+ (window as any).saveCredentials = saveCredentials;
7
+ (window as any).discoverDevices = discoverDevices
9
8
 
10
9
  // Initialize on page load
11
10
  async function init(): Promise<void> {
12
11
  await loadCredentialStatus()
13
12
  await initializeDiscoverySettings()
14
- const list = await loadConfiguredDevices()
13
+ await loadConfiguredDevices()
15
14
  await initRemoveAllButton()
16
15
  }
17
16