@switchbot/homebridge-switchbot 5.0.0-beta.133 → 5.0.0-beta.135

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 +67 -19
  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 +38 -30
  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 +69 -21
  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 +38 -29
  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,32 +431,80 @@ 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
- // Always use the platform-provided Matter type (e.g., 5 for roller shade)
437
- const matterType = 5
436
+ // Get current state for dynamic attributes
437
+ const state = await this.getState()
438
+ // Compose attributes for Matter WindowCovering cluster
438
439
  const attributes: Record<string, any> = {
439
- type: {
440
- read: async () => matterType,
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
+ },
441
472
  write: undefined,
442
473
  },
443
- currentPosition: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: undefined },
444
- 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) }) },
474
+ }
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
481
+ },
482
+ write: undefined,
483
+ }
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) }),
490
+ }
445
491
  }
446
492
  const windowCoveringCluster = {
447
493
  type: 'WindowCovering',
448
- matterType,
449
494
  clusterId: MATTER_CLUSTER_IDS.WindowCovering,
450
495
  attributes,
451
496
  }
452
- const clustersArr: any = [windowCoveringCluster]
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
453
501
  const foundWC = clustersArr.find((c: any) => c && c.clusterId === MATTER_CLUSTER_IDS.WindowCovering)
454
- clustersArr.windowCovering = foundWC || null
502
+ clusters.windowCovering = foundWC || null
455
503
  return {
456
504
  id: this.opts.id,
457
505
  name: this.opts.name ?? this.opts.type,
458
506
  protocol: 'matter',
459
- clusters: clustersArr,
507
+ clusters,
460
508
  }
461
509
  }
462
510
  }
@@ -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
 
@@ -15,7 +15,9 @@ export async function loadCredentialStatus(): Promise<void> {
15
15
  const tokenStatus = document.getElementById('tokenStatus')
16
16
  const secretStatus = document.getElementById('secretStatus')
17
17
 
18
- if (!tokenStatus || !secretStatus) { return }
18
+ if (!tokenStatus || !secretStatus) {
19
+ return
20
+ }
19
21
 
20
22
  if (creds.hasToken) {
21
23
  tokenStatus.textContent = `✓ Configured (${creds.tokenLength} characters)`
@@ -43,7 +45,9 @@ export async function saveCredentials(): Promise<void> {
43
45
  const saveStatus = document.getElementById('saveStatus')
44
46
  const saveBtn = document.getElementById('saveBtn') as HTMLButtonElement
45
47
 
46
- if (!saveStatus || !saveBtn) { return }
48
+ if (!saveStatus || !saveBtn) {
49
+ return
50
+ }
47
51
 
48
52
  if (!token || !secret) {
49
53
  saveStatus.textContent = 'Please enter both token and secret'
@@ -80,7 +84,7 @@ export async function saveCredentials(): Promise<void> {
80
84
  ;(document.getElementById('secret') as HTMLInputElement).value = ''
81
85
 
82
86
  // Reload status to verify save
83
- setTimeout(() => loadCredentialStatus(), 500)
87
+ setTimeout(loadCredentialStatus, 500)
84
88
 
85
89
  // Clear status message
86
90
  setTimeout(() => {
@@ -1,4 +1,4 @@
1
- import { deleteDevice as apiDeleteDevice, deleteAllDevices as apiDeleteAllDevices, syncParentPluginConfigFromDisk, fetchDevices } from './api.js'
1
+ import { deleteAllDevices as apiDeleteAllDevices, deleteDevice as apiDeleteDevice, fetchDevices, syncParentPluginConfigFromDisk } from './api.js'
2
2
  import { uiLog } from './logger.js'
3
3
  import { hideBusyUi, showBusyUi } from './modal.js'
4
4
  import { renderDeviceList } from './render.js'
@@ -10,7 +10,6 @@ type ImportDiscoveredDeviceResult = {
10
10
  address?: string
11
11
  encryptionKey?: string
12
12
  keyId?: string
13
- matterType?: number
14
13
  } | null
15
14
 
16
15
  export async function addDeviceToConfig(device: any, options: { refresh?: boolean, showStatus?: boolean } = {}): Promise<{ added: boolean }> {
@@ -32,7 +31,6 @@ export async function addDeviceToConfig(device: any, options: { refresh?: boolea
32
31
  rssi: device.rssi,
33
32
  encryptionKey: importValues.encryptionKey,
34
33
  keyId: importValues.keyId,
35
- matterType: importValues.matterType,
36
34
  })
37
35
  uiLog.info('Add device response:', resp)
38
36
 
@@ -1301,9 +1301,15 @@ async function updateDiscoveryView(
1301
1301
  const enableSelectedBtn = document.querySelector('button')?.parentElement?.querySelector('button[aria-label="Enable Selected"]') as HTMLButtonElement | null
1302
1302
  const disableSelectedBtn = document.querySelector('button')?.parentElement?.querySelector('button[aria-label="Disable Selected"]') as HTMLButtonElement | null
1303
1303
  const hasSelection = selectedIds.size > 0
1304
- if (addSelectedBtn) addSelectedBtn.disabled = !hasSelection
1305
- if (enableSelectedBtn) enableSelectedBtn.disabled = !hasSelection
1306
- if (disableSelectedBtn) disableSelectedBtn.disabled = !hasSelection
1304
+ if (addSelectedBtn) {
1305
+ addSelectedBtn.disabled = !hasSelection
1306
+ }
1307
+ if (enableSelectedBtn) {
1308
+ enableSelectedBtn.disabled = !hasSelection
1309
+ }
1310
+ if (disableSelectedBtn) {
1311
+ disableSelectedBtn.disabled = !hasSelection
1312
+ }
1307
1313
  }
1308
1314
  window.removeEventListener('discovery-selection-changed', updateBatchButtonStates)
1309
1315
  window.addEventListener('discovery-selection-changed', updateBatchButtonStates)
@@ -1,11 +1,17 @@
1
+ /* eslint-disable no-console */
2
+ const PREFIX = '[SwitchBot UI/html]'
3
+
1
4
  export const uiLog = {
2
- info: (message: string, ...parameters: any[]): void => {
3
- console.warn('[SwitchBot UI]', message, ...parameters)
5
+ info: (message: string, ...parameters: any[]) => {
6
+ console.log(PREFIX, message, ...parameters)
4
7
  },
5
- warn: (message: string, ...parameters: any[]): void => {
6
- console.warn('[SwitchBot UI]', message, ...parameters)
8
+ warn: (message: string, ...parameters: any[]) => {
9
+ console.warn(PREFIX, message, ...parameters)
7
10
  },
8
- error: (message: string, ...parameters: any[]): void => {
9
- console.error('[SwitchBot UI]', message, ...parameters)
11
+ error: (message: string, ...parameters: any[]) => {
12
+ console.error(PREFIX, message, ...parameters)
13
+ },
14
+ debug: (message: string, ...parameters: any[]) => {
15
+ console.debug(PREFIX, message, ...parameters)
10
16
  },
11
17
  }