@switchbot/homebridge-switchbot 5.0.0-beta.110 → 5.0.0-beta.112

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 (57) hide show
  1. package/.github/copilot-instructions.md +39 -0
  2. package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
  3. package/dist/homebridge-ui/endpoints/devices.js +10 -1
  4. package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
  5. package/dist/homebridge-ui/public/css/styles.css +111 -16
  6. package/dist/homebridge-ui/public/index.html +34 -60
  7. package/dist/homebridge-ui/public/js/api.d.ts +5 -1
  8. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  9. package/dist/homebridge-ui/public/js/api.js +13 -3
  10. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  11. package/dist/homebridge-ui/public/js/api.ts +13 -3
  12. package/dist/homebridge-ui/public/js/app.js +338 -42
  13. package/dist/homebridge-ui/public/js/app.js.map +3 -3
  14. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
  15. package/dist/homebridge-ui/public/js/devices.js +12 -3
  16. package/dist/homebridge-ui/public/js/devices.js.map +1 -1
  17. package/dist/homebridge-ui/public/js/devices.ts +13 -3
  18. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
  19. package/dist/homebridge-ui/public/js/discovery.js +37 -3
  20. package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
  21. package/dist/homebridge-ui/public/js/discovery.ts +39 -2
  22. package/dist/homebridge-ui/public/js/modals.d.ts +5 -0
  23. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
  24. package/dist/homebridge-ui/public/js/modals.js +146 -0
  25. package/dist/homebridge-ui/public/js/modals.js.map +1 -1
  26. package/dist/homebridge-ui/public/js/modals.ts +167 -0
  27. package/dist/homebridge-ui/public/js/render.d.ts +3 -1
  28. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
  29. package/dist/homebridge-ui/public/js/render.js +135 -32
  30. package/dist/homebridge-ui/public/js/render.js.map +1 -1
  31. package/dist/homebridge-ui/public/js/render.ts +145 -32
  32. package/dist/platform.d.ts.map +1 -1
  33. package/dist/platform.js +64 -109
  34. package/dist/platform.js.map +1 -1
  35. package/dist/utils.d.ts.map +1 -1
  36. package/dist/utils.js +2 -1
  37. package/dist/utils.js.map +1 -1
  38. package/docs/variables/default.html +1 -1
  39. package/package.json +9 -8
  40. package/src/homebridge-ui/endpoints/devices.ts +5 -2
  41. package/src/homebridge-ui/public/css/styles.css +111 -16
  42. package/src/homebridge-ui/public/index.html +34 -60
  43. package/src/homebridge-ui/public/js/api.ts +13 -3
  44. package/src/homebridge-ui/public/js/devices.ts +13 -3
  45. package/src/homebridge-ui/public/js/discovery.ts +39 -2
  46. package/src/homebridge-ui/public/js/modals.ts +167 -0
  47. package/src/homebridge-ui/public/js/render.ts +145 -32
  48. package/src/platform.ts +77 -109
  49. package/src/utils.ts +2 -1
  50. package/test/e2e/run-e2e.spec.ts +12 -10
  51. package/test/helpers/matter-harness.ts +2 -2
  52. package/test/matter-childbridge.spec.ts +5 -1
  53. package/test/matter-integration.spec.ts +41 -0
  54. package/test/utils.spec.ts +18 -2
  55. package/todo/COMPLETED.md +53 -0
  56. package/todo/TODO.md +13 -42
  57. package/switchbot-homebridge-switchbot-5.0.0.tgz +0 -0
@@ -154,8 +154,34 @@ export function renderIRBadge(): HTMLElement {
154
154
  )
155
155
  }
156
156
 
157
- export async function renderDiscoveredDevices(devices: any[]): Promise<HTMLElement> {
157
+ function normalizeId(value: any): string {
158
+ return String(value ?? '').trim().toLowerCase()
159
+ }
160
+
161
+ function scrollToConfiguredDevice(deviceId: string): void {
162
+ const normalizedId = normalizeId(deviceId)
163
+ const target = document.querySelector(`[data-device-id="${normalizedId}"]`) as HTMLElement | null
164
+ if (!target) {
165
+ return
166
+ }
167
+
168
+ target.scrollIntoView({ behavior: 'smooth', block: 'center' })
169
+ const originalOutline = target.style.outline
170
+ const originalBackground = target.style.background
171
+ target.style.outline = '2px solid var(--switchbot-red, #ef4444)'
172
+ target.style.background = 'rgba(239, 68, 68, 0.08)'
173
+ setTimeout(() => {
174
+ target.style.outline = originalOutline
175
+ target.style.background = originalBackground
176
+ }, 1800)
177
+ }
178
+
179
+ export async function renderDiscoveredDevices(
180
+ devices: any[],
181
+ options: { configuredIds?: Set<string> } = {},
182
+ ): Promise<HTMLElement> {
158
183
  const ul = document.createElement('ul')
184
+ ul.className = 'device-grid'
159
185
  ul.style.maxHeight = '400px'
160
186
  ul.style.overflowY = 'auto'
161
187
  ul.style.marginTop = '12px'
@@ -163,35 +189,50 @@ export async function renderDiscoveredDevices(devices: any[]): Promise<HTMLEleme
163
189
  ul.style.listStyle = 'none'
164
190
 
165
191
  const { addDeviceToConfig } = await import('./discovery.js')
192
+ const { loadConfiguredDevices } = await import('./devices.js')
193
+ const configuredIds = options.configuredIds ?? new Set<string>()
166
194
 
167
195
  for (const d of devices) {
196
+ const deviceId = normalizeId(d.id)
197
+ const alreadyAdded = configuredIds.has(deviceId)
198
+
168
199
  const li = document.createElement('li')
169
200
  li.className = 'device-item'
170
201
  li.style.display = 'flex'
171
- li.style.alignItems = 'center'
172
- li.style.justifyContent = 'space-between'
173
- li.style.padding = '10px 12px'
174
- li.style.marginBottom = '6px'
175
- li.style.borderRadius = '6px'
202
+ li.style.flexDirection = 'column'
203
+ li.style.alignItems = 'stretch'
204
+ li.style.justifyContent = 'flex-start'
205
+ li.style.padding = '5px 8px'
206
+ li.style.marginBottom = '0'
207
+ li.style.borderRadius = '5px'
176
208
  li.style.transition = 'all 0.2s ease'
177
209
 
178
210
  const info = document.createElement('div')
179
- info.style.flex = '1'
211
+ info.style.flex = '1 1 auto'
212
+ info.style.width = '100%'
213
+ info.style.minWidth = '0'
180
214
 
181
215
  const nameContainer = document.createElement('div')
182
216
  nameContainer.style.display = 'flex'
183
217
  nameContainer.style.alignItems = 'center'
184
- nameContainer.style.marginBottom = '4px'
218
+ nameContainer.style.marginBottom = '0'
185
219
  nameContainer.style.flexWrap = 'wrap'
186
- nameContainer.style.gap = '6px'
220
+ nameContainer.style.gap = '4px'
187
221
 
188
222
  const name = document.createElement('div')
189
223
  name.style.fontWeight = '500'
190
- name.style.fontSize = '14px'
224
+ name.style.fontSize = '13px'
191
225
  name.textContent = d.name || d.id
192
226
 
193
227
  nameContainer.appendChild(name)
194
228
 
229
+ const duplicateBadge = document.createElement('span')
230
+ duplicateBadge.textContent = alreadyAdded ? '✓ Already Added' : '➕ New Device'
231
+ duplicateBadge.style.cssText = alreadyAdded
232
+ ? 'background: #16a34a; color: white; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600;'
233
+ : 'background: #2563eb; color: white; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600;'
234
+ nameContainer.appendChild(duplicateBadge)
235
+
195
236
  // Add connection type badge
196
237
  if (d.connectionType) {
197
238
  const badge = renderConnectionBadge(d.connectionType)
@@ -210,10 +251,13 @@ export async function renderDiscoveredDevices(devices: any[]): Promise<HTMLEleme
210
251
  }
211
252
 
212
253
  const details = document.createElement('div')
213
- details.style.fontSize = '11px'
254
+ details.style.fontSize = '10px'
214
255
  details.style.opacity = '0.7'
215
- details.style.marginTop = '4px'
256
+ details.style.marginTop = '0'
216
257
  details.style.fontFamily = 'monospace'
258
+ details.style.whiteSpace = 'normal'
259
+ details.style.overflowWrap = 'anywhere'
260
+ details.style.wordBreak = 'break-word'
217
261
 
218
262
  let detailsText = `ID: ${d.id} | Type: ${d.type} | Model: ${d.model || 'N/A'}`
219
263
  if (d.hubDeviceId) { detailsText += ` | Hub: ${d.hubDeviceId}` }
@@ -224,18 +268,68 @@ export async function renderDiscoveredDevices(devices: any[]): Promise<HTMLEleme
224
268
  info.appendChild(details)
225
269
 
226
270
  const addBtn = document.createElement('button')
227
- addBtn.textContent = '+ Add'
228
- addBtn.style.marginLeft = '8px'
229
- addBtn.style.padding = '6px 14px'
230
- addBtn.style.fontSize = '12px'
271
+ addBtn.textContent = alreadyAdded ? 'Already Added' : 'Add to Config'
272
+ addBtn.style.marginLeft = '0'
273
+ addBtn.style.marginTop = '2px'
274
+ addBtn.style.padding = '4px 9px'
275
+ addBtn.style.fontSize = '11px'
231
276
  addBtn.style.whiteSpace = 'nowrap'
232
277
  addBtn.style.flexShrink = '0'
278
+ addBtn.disabled = alreadyAdded
279
+ if (alreadyAdded) {
280
+ addBtn.style.opacity = '0.65'
281
+ addBtn.style.cursor = 'not-allowed'
282
+ addBtn.style.background = '#6b7280'
283
+ }
233
284
  addBtn.onclick = async () => {
285
+ if (alreadyAdded) {
286
+ return
287
+ }
234
288
  await addDeviceToConfig(d)
235
289
  }
236
290
 
291
+ if (alreadyAdded) {
292
+ const viewBtn = document.createElement('button')
293
+ viewBtn.textContent = 'View in Config'
294
+ viewBtn.className = 'secondary'
295
+ viewBtn.style.marginLeft = '0'
296
+ viewBtn.style.padding = '4px 9px'
297
+ viewBtn.style.fontSize = '11px'
298
+ viewBtn.onclick = async () => {
299
+ await loadConfiguredDevices()
300
+ scrollToConfiguredDevice(d.id)
301
+ }
302
+ li.appendChild(info)
303
+ const actions = document.createElement('div')
304
+ actions.className = 'device-actions'
305
+ actions.style.display = 'flex'
306
+ actions.style.alignItems = 'center'
307
+ actions.style.flexWrap = 'wrap'
308
+ actions.style.justifyContent = 'flex-start'
309
+ actions.style.marginLeft = '0'
310
+ actions.style.width = '100%'
311
+ actions.style.marginTop = '2px'
312
+ actions.style.gap = '5px'
313
+ actions.appendChild(viewBtn)
314
+ actions.appendChild(addBtn)
315
+ li.appendChild(actions)
316
+ ul.appendChild(li)
317
+ continue
318
+ }
319
+
320
+ const actions = document.createElement('div')
321
+ actions.className = 'device-actions'
322
+ actions.style.display = 'flex'
323
+ actions.style.flexWrap = 'wrap'
324
+ actions.style.justifyContent = 'flex-start'
325
+ actions.style.marginLeft = '0'
326
+ actions.style.width = '100%'
327
+ actions.style.marginTop = '2px'
328
+ actions.style.gap = '5px'
329
+ actions.appendChild(addBtn)
330
+
237
331
  li.appendChild(info)
238
- li.appendChild(addBtn)
332
+ li.appendChild(actions)
239
333
  ul.appendChild(li)
240
334
  }
241
335
 
@@ -406,6 +500,7 @@ export function renderDeviceList(list: any[]): void {
406
500
  }
407
501
 
408
502
  status.textContent = `Found ${list.length} device(s)`
503
+ ul.classList.add('device-grid')
409
504
  ul.innerHTML = ''
410
505
 
411
506
  // Show remove all button when devices exist
@@ -416,25 +511,37 @@ export function renderDeviceList(list: any[]): void {
416
511
  for (const d of list) {
417
512
  const li = document.createElement('li')
418
513
  li.className = 'device-item'
514
+ li.setAttribute('data-device-id', normalizeId(d.id))
515
+ li.style.display = 'flex'
516
+ li.style.flexDirection = 'column'
517
+ li.style.alignItems = 'stretch'
518
+ li.style.marginBottom = '0'
419
519
 
420
520
  const info = document.createElement('div')
421
- info.style.flex = '1'
521
+ info.style.flex = '1 1 auto'
522
+ info.style.width = '100%'
523
+ info.style.minWidth = '0'
422
524
 
423
525
  const nameContainer = document.createElement('div')
424
526
  nameContainer.style.display = 'flex'
425
- nameContainer.style.alignItems = 'center'
426
- nameContainer.style.flexWrap = 'wrap'
427
- nameContainer.style.gap = '6px'
527
+ nameContainer.style.flexDirection = 'column'
528
+ nameContainer.style.alignItems = 'flex-start'
529
+ nameContainer.style.gap = '0'
428
530
 
429
531
  const name = document.createElement('div')
430
532
  name.style.fontWeight = '500'
533
+ name.style.fontSize = '13px'
431
534
  name.textContent = d.name || d.id
432
535
 
433
536
  const code = document.createElement('code')
434
537
  code.textContent = d.id
435
- code.style.fontSize = '12px'
538
+ code.style.fontSize = '10px'
436
539
  code.style.opacity = '0.75'
437
- code.style.marginLeft = '8px'
540
+ code.style.marginLeft = '0'
541
+ code.style.whiteSpace = 'normal'
542
+ code.style.overflowWrap = 'anywhere'
543
+ code.style.wordBreak = 'break-word'
544
+ code.style.maxWidth = '100%'
438
545
 
439
546
  nameContainer.appendChild(name)
440
547
  nameContainer.appendChild(code)
@@ -447,8 +554,8 @@ export function renderDeviceList(list: any[]): void {
447
554
 
448
555
  const meta = document.createElement('div')
449
556
  meta.style.opacity = '0.75'
450
- meta.style.marginTop = '4px'
451
- meta.style.fontSize = '12px'
557
+ meta.style.marginTop = '0'
558
+ meta.style.fontSize = '11px'
452
559
 
453
560
  const typeText = d.type ? `type: ${d.type}` : ''
454
561
  const connText = d.connectionPreference ? `conn: ${d.connectionPreference}` : ''
@@ -459,13 +566,19 @@ export function renderDeviceList(list: any[]): void {
459
566
  info.appendChild(meta)
460
567
 
461
568
  const buttons = document.createElement('div')
569
+ buttons.className = 'device-actions'
462
570
  buttons.style.display = 'flex'
463
- buttons.style.gap = '8px'
571
+ buttons.style.flexWrap = 'wrap'
572
+ buttons.style.justifyContent = 'flex-start'
573
+ buttons.style.marginLeft = '0'
574
+ buttons.style.width = '100%'
575
+ buttons.style.marginTop = '2px'
576
+ buttons.style.gap = '5px'
464
577
 
465
578
  const editBtn = document.createElement('button')
466
579
  editBtn.textContent = '✏️ Edit'
467
- editBtn.style.padding = '6px 12px'
468
- editBtn.style.fontSize = '12px'
580
+ editBtn.style.padding = '4px 9px'
581
+ editBtn.style.fontSize = '11px'
469
582
  editBtn.onclick = async () => {
470
583
  const { editDevice } = await import('./modals.js')
471
584
  await editDevice(d)
@@ -473,8 +586,8 @@ export function renderDeviceList(list: any[]): void {
473
586
 
474
587
  const copyBtn = document.createElement('button')
475
588
  copyBtn.textContent = 'Copy ID'
476
- copyBtn.style.padding = '6px 12px'
477
- copyBtn.style.fontSize = '12px'
589
+ copyBtn.style.padding = '4px 9px'
590
+ copyBtn.style.fontSize = '11px'
478
591
  copyBtn.addEventListener('click', async () => {
479
592
  try {
480
593
  await navigator.clipboard.writeText(d.id)
@@ -491,8 +604,8 @@ export function renderDeviceList(list: any[]): void {
491
604
 
492
605
  const deleteBtn = document.createElement('button')
493
606
  deleteBtn.textContent = '🗑️ Delete'
494
- deleteBtn.style.padding = '6px 12px'
495
- deleteBtn.style.fontSize = '12px'
607
+ deleteBtn.style.padding = '4px 9px'
608
+ deleteBtn.style.fontSize = '11px'
496
609
  deleteBtn.style.background = '#ef4444'
497
610
  deleteBtn.onclick = async () => {
498
611
  const { deleteDeviceFromConfig } = await import('./devices-delete.js')
package/src/platform.ts CHANGED
@@ -97,7 +97,9 @@ const DEVICE_MATTER_CLUSTERS: Record<string, any> = {
97
97
  }, // Switch in HAP, RobotVacuumCleaner in Matter
98
98
  curtain: {
99
99
  windowCovering: {
100
- type: 4, // Drapery - safe default for LF conformance
100
+ // Use a lift-only/non-tilt covering type to satisfy Matter conformance rules.
101
+ // Drapery (4) can fail validation in some stacks when TL (tilt) is not present.
102
+ type: 0, // Rollershade
101
103
  },
102
104
  }, // WindowCovering → WindowCovering (includes curtain3, rollershade variants via normalization)
103
105
  blindtilt: {
@@ -130,12 +132,7 @@ const DEVICE_MATTER_CLUSTERS: Record<string, any> = {
130
132
  minLevel: 0,
131
133
  maxLevel: 254,
132
134
  },
133
- colorControl: {
134
- currentHue: 0,
135
- currentSaturation: 0,
136
- colorTemperatureMireds: 400,
137
- colorMode: 0,
138
- },
135
+ colorControl: {},
139
136
  }, // Lightbulb with color → OnOff + LevelControl + ColorControl
140
137
  lock: {
141
138
  doorLock: {
@@ -226,6 +223,76 @@ const DEVICE_MATTER_DEVICE_TYPE_KEYS: Record<string, string> = {
226
223
  waterdetector: 'LeakSensor',
227
224
  }
228
225
 
226
+ function normalizeTypeForMatter(typeValue: string | undefined | null): string {
227
+ const raw = String(typeValue || '').trim().toLowerCase()
228
+ if (!raw) {
229
+ return 'unknown'
230
+ }
231
+
232
+ // Vacuum variants
233
+ if (['wosweeper', 'wosweepermini', 'wosweeperminipro', 'k10+', 'k10+ pro'].includes(raw)) {
234
+ return 'vacuum'
235
+ }
236
+
237
+ // Window covering variants
238
+ if (['blindtilt', 'blind tilt', 'curtain3', 'rollershade', 'roller shade', 'worollershade', 'wo rollershade'].includes(raw)) {
239
+ return 'curtain'
240
+ }
241
+
242
+ // Plug variants
243
+ if (['plug mini (jp)', 'plug mini (us)', 'plug mini (eu)'].includes(raw)) {
244
+ return 'plug'
245
+ }
246
+
247
+ // Meter variants
248
+ if (['meterplus', 'meter plus', 'meter plus (jp)', 'meterpro', 'meter pro', 'meterpro(co2)', 'meter pro (co2)'].includes(raw)) {
249
+ return 'meter'
250
+ }
251
+
252
+ // Relay switch variants
253
+ if (['relay switch 1', 'relay switch 1pm'].includes(raw)) {
254
+ return 'relay'
255
+ }
256
+
257
+ // Water detector variants
258
+ if (['water detector', 'waterdetector'].includes(raw)) {
259
+ return 'waterdetector'
260
+ }
261
+
262
+ // Fan variants
263
+ if (['smart fan', 'circulator fan', 'battery circulator fan', 'standing circulator fan'].includes(raw)) {
264
+ return 'fan'
265
+ }
266
+
267
+ // Light variants
268
+ if (['strip light', 'strip light 3', 'rgbic neon rope light', 'rgbic neon wire rope light', 'rgbicww floor lamp', 'rgbicww strip light'].includes(raw)) {
269
+ return 'lightstrip'
270
+ }
271
+ if (['color bulb', 'ceiling light', 'ceiling light pro', 'candle warmer lamp', 'floor lamp'].includes(raw)) {
272
+ return 'light'
273
+ }
274
+
275
+ // Sensor variants
276
+ if (raw === 'motion sensor') {
277
+ return 'motion'
278
+ }
279
+ if (['contact sensor', 'presence sensor'].includes(raw)) {
280
+ return 'contact'
281
+ }
282
+
283
+ // Lock variants
284
+ if (['smart lock', 'smart lock pro', 'smart lock ultra', 'lock lite', 'keypad', 'keypad touch', 'keypad vision', 'keypad vision pro', 'lock vision pro'].includes(raw)) {
285
+ return 'lock'
286
+ }
287
+
288
+ // Climate variant
289
+ if (raw === 'humidifier2') {
290
+ return 'humidifier'
291
+ }
292
+
293
+ return raw
294
+ }
295
+
229
296
  // Factory function to create Matter handlers with Homebridge logger integration
230
297
  function createMatterHandlers(log: Logger, deviceId: string, type: string, client: any): any {
231
298
  const lowerType = type.toLowerCase()
@@ -1030,40 +1097,7 @@ export class SwitchBotHAPPlatform {
1030
1097
  _raw: raw,
1031
1098
  }
1032
1099
 
1033
- let type: string = d.type
1034
- // Normalize device type variants for consistent Matter cluster lookup
1035
- // Vacuum variants
1036
- if (['wosweeper', 'wosweepermini', 'wosweeperminipro', 'k10+', 'k10+ pro'].includes((type || '').toLowerCase())) {
1037
- type = 'vacuum'
1038
- }
1039
- // Window covering variants
1040
- if (['blindtilt', 'blind tilt', 'curtain3', 'rollershade', 'roller shade', 'worollershade', 'wo rollershade'].includes((type || '').toLowerCase())) {
1041
- type = 'curtain'
1042
- }
1043
- // Plug variants
1044
- if (['plug mini (jp)', 'plug mini (us)'].includes((type || '').toLowerCase())) {
1045
- type = 'plug'
1046
- }
1047
- // Meter variants
1048
- if (['meterplus', 'meter plus (jp)', 'meterpro', 'meterpro(co2)'].includes((type || '').toLowerCase())) {
1049
- type = 'meter'
1050
- }
1051
- // Relay switch variants
1052
- if (['relay switch 1', 'relay switch 1pm'].includes((type || '').toLowerCase())) {
1053
- type = 'relay'
1054
- }
1055
- // Water detector variants
1056
- if (['water detector'].includes((type || '').toLowerCase())) {
1057
- type = 'waterdetector'
1058
- }
1059
- // Fan variants
1060
- if (['smart fan'].includes((type || '').toLowerCase())) {
1061
- type = 'fan'
1062
- }
1063
- // Light variants
1064
- if (['strip light'].includes((type || '').toLowerCase())) {
1065
- type = 'lightstrip'
1066
- }
1100
+ const type: string = normalizeTypeForMatter(d.type)
1067
1101
 
1068
1102
  const matterSupported = !!DEVICE_MATTER_SUPPORTED[(type || '').toLowerCase()]
1069
1103
  // Auto-detect Matter from Homebridge API, allow manual override via config
@@ -1307,40 +1341,7 @@ export class SwitchBotMatterPlatform {
1307
1341
  _raw: raw,
1308
1342
  }
1309
1343
 
1310
- let type: string = d.type
1311
- // Normalize device type variants for consistent Matter cluster lookup
1312
- // Vacuum variants
1313
- if (['wosweeper', 'wosweepermini', 'wosweeperminipro', 'k10+', 'k10+ pro'].includes((type || '').toLowerCase())) {
1314
- type = 'vacuum'
1315
- }
1316
- // Window covering variants
1317
- if (['blindtilt', 'blind tilt', 'curtain3', 'rollershade', 'roller shade', 'worollershade', 'wo rollershade'].includes((type || '').toLowerCase())) {
1318
- type = 'curtain'
1319
- }
1320
- // Plug variants
1321
- if (['plug mini (jp)', 'plug mini (us)'].includes((type || '').toLowerCase())) {
1322
- type = 'plug'
1323
- }
1324
- // Meter variants
1325
- if (['meterplus', 'meter plus (jp)', 'meterpro', 'meterpro(co2)'].includes((type || '').toLowerCase())) {
1326
- type = 'meter'
1327
- }
1328
- // Relay switch variants
1329
- if (['relay switch 1', 'relay switch 1pm'].includes((type || '').toLowerCase())) {
1330
- type = 'relay'
1331
- }
1332
- // Water detector variants
1333
- if (['water detector'].includes((type || '').toLowerCase())) {
1334
- type = 'waterdetector'
1335
- }
1336
- // Fan variants
1337
- if (['smart fan'].includes((type || '').toLowerCase())) {
1338
- type = 'fan'
1339
- }
1340
- // Light variants
1341
- if (['strip light'].includes((type || '').toLowerCase())) {
1342
- type = 'lightstrip'
1343
- }
1344
+ const type: string = normalizeTypeForMatter(d.type)
1344
1345
 
1345
1346
  const matterSupported = !!DEVICE_MATTER_SUPPORTED[(type || '').toLowerCase()]
1346
1347
  // Auto-detect Matter from Homebridge API, allow manual override via config
@@ -1439,40 +1440,7 @@ export class SwitchBotMatterPlatform {
1439
1440
  continue
1440
1441
  }
1441
1442
 
1442
- let type: string = d.type
1443
- // Normalize device type variants for consistent Matter cluster lookup
1444
- // Vacuum variants
1445
- if (['wosweeper', 'wosweepermini', 'wosweeperminipro', 'k10+', 'k10+ pro'].includes((type || '').toLowerCase())) {
1446
- type = 'vacuum'
1447
- }
1448
- // Window covering variants
1449
- if (['blindtilt', 'blind tilt', 'curtain3', 'rollershade', 'roller shade', 'worollershade', 'wo rollershade'].includes((type || '').toLowerCase())) {
1450
- type = 'curtain'
1451
- }
1452
- // Plug variants
1453
- if (['plug mini (jp)', 'plug mini (us)'].includes((type || '').toLowerCase())) {
1454
- type = 'plug'
1455
- }
1456
- // Meter variants
1457
- if (['meterplus', 'meter plus (jp)', 'meterpro', 'meterpro(co2)'].includes((type || '').toLowerCase())) {
1458
- type = 'meter'
1459
- }
1460
- // Relay switch variants
1461
- if (['relay switch 1', 'relay switch 1pm'].includes((type || '').toLowerCase())) {
1462
- type = 'relay'
1463
- }
1464
- // Water detector variants
1465
- if (['water detector'].includes((type || '').toLowerCase())) {
1466
- type = 'waterdetector'
1467
- }
1468
- // Fan variants
1469
- if (['smart fan'].includes((type || '').toLowerCase())) {
1470
- type = 'fan'
1471
- }
1472
- // Light variants
1473
- if (['strip light'].includes((type || '').toLowerCase())) {
1474
- type = 'lightstrip'
1475
- }
1443
+ const type: string = normalizeTypeForMatter(d.type)
1476
1444
 
1477
1445
  const matterSupported = !!DEVICE_MATTER_SUPPORTED[(type || '').toLowerCase()]
1478
1446
  const useMatter = matterEnabled && matterSupported
package/src/utils.ts CHANGED
@@ -36,8 +36,9 @@ export function createPlatformProxy(HAPPlatform: any, MatterPlatform: any): any
36
36
  const cfg = normalizeConfig(config)
37
37
  const preferMatter = cfg.preferMatter ?? true
38
38
  const enableMatter = cfg.enableMatter ?? true
39
+ const matterAvailable = !!(api?.isMatterAvailable?.() && api?.isMatterEnabled?.())
39
40
 
40
- if (enableMatter && preferMatter && MatterPlatform) {
41
+ if (enableMatter && preferMatter && MatterPlatform && matterAvailable) {
41
42
  this.impl = new MatterPlatform(log, cfg, api)
42
43
  return this.impl
43
44
  }
@@ -1,21 +1,21 @@
1
- import { describe, test } from 'vitest'
2
- import { promisify } from 'util'
3
- import { execFile } from 'child_process'
4
- import path from 'path'
1
+ import { execFile } from 'node:child_process'
2
+ import path from 'node:path'
3
+ import { promisify } from 'node:util'
4
+ import { describe, it } from 'vitest'
5
5
 
6
6
  const execFileP = promisify(execFile)
7
7
 
8
- describe('E2E scripts (manual-run harness)', () => {
8
+ describe('e2e scripts (manual-run harness)', () => {
9
9
  if (!process.env.RUN_E2E) {
10
- test.skip('E2E disabled - set RUN_E2E=true to enable', () => {})
10
+ it.skip('e2e disabled - set RUN_E2E=true to enable', () => {})
11
11
  return
12
12
  }
13
13
 
14
- test('run configured E2E scripts sequentially', async () => {
14
+ it('should run configured e2e scripts sequentially', async () => {
15
15
  const repoRoot = path.resolve(__dirname, '../../')
16
16
  const scriptsDir = path.join(repoRoot, 'scripts', 'e2e')
17
17
 
18
- const scriptsToRun: { envVar: string; script: string }[] = [
18
+ const scriptsToRun: { envVar: string, script: string }[] = [
19
19
  { envVar: 'LIGHT_ACCESSORY_ID', script: 'light-e2e.sh' },
20
20
  { envVar: 'FAN_ACCESSORY_ID', script: 'fan-e2e.sh' },
21
21
  { envVar: 'CURTAIN_ACCESSORY_ID', script: 'curtain-e2e.sh' },
@@ -32,7 +32,7 @@ describe('E2E scripts (manual-run harness)', () => {
32
32
 
33
33
  const scriptPath = path.join(scriptsDir, s.script)
34
34
  // Forward environment so scripts can read HB_URL / HB_TOKEN / <ACCESSORY_ID>
35
- const env = Object.assign({}, process.env)
35
+ const env = { ...process.env }
36
36
 
37
37
  try {
38
38
  // eslint-disable-next-line no-console
@@ -40,7 +40,9 @@ describe('E2E scripts (manual-run harness)', () => {
40
40
  const { stdout, stderr } = await execFileP('bash', [scriptPath], { env })
41
41
  // eslint-disable-next-line no-console
42
42
  console.log(stdout)
43
- if (stderr) console.error(stderr)
43
+ if (stderr) {
44
+ console.error(stderr)
45
+ }
44
46
  } catch (e: any) {
45
47
  // bubble up as test failure
46
48
  throw new Error(`Script ${s.script} failed: ${e.message}`)
@@ -47,7 +47,7 @@ export function makeFakeApiWithoutMatter() {
47
47
  isMatterEnabled: () => false,
48
48
  hap: makeFakeHap(),
49
49
  platformAccessory: PlatformAccessory,
50
- registerPlatformAccessories: (_p: string, _n: string, accs: any[]) => {},
51
- on: (_ev: string, cb: Function) => {},
50
+ registerPlatformAccessories: () => {},
51
+ on: () => {},
52
52
  }
53
53
  }
@@ -31,7 +31,11 @@ describe('platform integration (Matter)', () => {
31
31
 
32
32
  // Create proxy that would choose the fake matter platform when preferred
33
33
  const Proxy = createPlatformProxy(function HAP() {}, FakeMatterPlatform)
34
- const platform = new (Proxy as any)(console, { preferMatter: true, enableMatter: true }, {})
34
+ const matterApi = {
35
+ isMatterAvailable: () => true,
36
+ isMatterEnabled: () => true,
37
+ }
38
+ const platform = new (Proxy as any)(console, { preferMatter: true, enableMatter: true }, matterApi)
35
39
 
36
40
  // Make a small device descriptor to load
37
41
  const device = { id: 'dev-m-1', type: 'light', name: 'MatterLight' }
@@ -6,6 +6,47 @@ import SwitchBotHAPPlatform, { SwitchBotMatterPlatform } from '../src/platform'
6
6
  // Expanded Matter integration harness tests
7
7
 
8
8
  describe('Matter integration harness', () => {
9
+ it('normalizes Color Bulb and Motion Sensor to Matter-supported categories', async () => {
10
+ const matterApi = makeFakeMatterApi()
11
+ const api = makeFakeApiWithMatter(matterApi)
12
+
13
+ const platform = new SwitchBotMatterPlatform(console as any, {
14
+ devices: [
15
+ { id: 'cb-1', type: 'Color Bulb', name: 'Color Bulb 1' },
16
+ { id: 'ms-1', type: 'Motion Sensor', name: 'Motion Sensor 1' },
17
+ ],
18
+ enableMatter: true,
19
+ preferMatter: true,
20
+ } as any, api as any)
21
+
22
+ await (platform as any).registerMatterAccessories()
23
+
24
+ const regs = matterApi.getRegistered()
25
+ expect(regs.length).toBeGreaterThanOrEqual(2)
26
+ const ids = regs.map((r: any) => r?.context?.deviceId)
27
+ expect(ids).toContain('cb-1')
28
+ expect(ids).toContain('ms-1')
29
+ })
30
+
31
+ it('uses conformant window covering type for curtain Matter registration', async () => {
32
+ const matterApi = makeFakeMatterApi()
33
+ const api = makeFakeApiWithMatter(matterApi)
34
+
35
+ const platform = new SwitchBotMatterPlatform(console as any, {
36
+ devices: [{ id: 'curtain-1', type: 'curtain', name: 'Curtain 1' }],
37
+ enableMatter: true,
38
+ preferMatter: true,
39
+ } as any, api as any)
40
+
41
+ await (platform as any).registerMatterAccessories()
42
+
43
+ const regs = matterApi.getRegistered()
44
+ expect(regs.length).toBeGreaterThanOrEqual(1)
45
+ const wcType = regs[0]?.clusters?.windowCovering?.type
46
+ expect(wcType).toBe(0)
47
+ expect(wcType).not.toBe(4)
48
+ })
49
+
9
50
  it('registers Matter accessories when Matter API available', async () => {
10
51
  // Instantiate the real Matter platform directly to test its registration path
11
52
  const matterApi = makeFakeMatterApi()