@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')
@@ -1 +1 @@
1
- {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAC1D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAk7B7D,qBAAa,oBAAoB;IAC/B,GAAG,EAAE,GAAG,GAAG,SAAS,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,qBAAqB,CAAA;IAC7B,OAAO,EAAE,GAAG,EAAE,CAAK;IAEnB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAE7B,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,oBAAoB,CAA8B;gBAE9C,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,GAAG;IA2C1D,OAAO,CAAC,aAAa;YAUP,qBAAqB;IAU7B,WAAW;IAqMX,kBAAkB,CAAC,SAAS,EAAE,GAAG;IAYvC,wBAAwB,CAAC,CAAC,SAAS,EAAE,GAAG;CASzC;AAGD,qBAAa,uBAAuB;IAClC,GAAG,EAAE,GAAG,GAAG,SAAS,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,qBAAqB,CAAA;IAC7B,OAAO,EAAE,GAAG,EAAE,CAAK;IACnB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAE7B,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,oBAAoB,CAA8B;gBAE9C,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,GAAG;IA8CpD,WAAW;IAyEjB,OAAO,CAAC,aAAa;YAUP,qBAAqB;IAU7B,kBAAkB,CAAC,SAAS,EAAE,GAAG;IAWvC,wBAAwB,CAAC,SAAS,EAAE,GAAG;IAWjC,yBAAyB;CAsJhC;AAED,eAAe,oBAAoB,CAAA"}
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAC1D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAq/B7D,qBAAa,oBAAoB;IAC/B,GAAG,EAAE,GAAG,GAAG,SAAS,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,qBAAqB,CAAA;IAC7B,OAAO,EAAE,GAAG,EAAE,CAAK;IAEnB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAE7B,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,oBAAoB,CAA8B;gBAE9C,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,GAAG;IA2C1D,OAAO,CAAC,aAAa;YAUP,qBAAqB;IAU7B,WAAW;IAoKX,kBAAkB,CAAC,SAAS,EAAE,GAAG;IAYvC,wBAAwB,CAAC,CAAC,SAAS,EAAE,GAAG;CASzC;AAGD,qBAAa,uBAAuB;IAClC,GAAG,EAAE,GAAG,GAAG,SAAS,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,qBAAqB,CAAA;IAC7B,OAAO,EAAE,GAAG,EAAE,CAAK;IACnB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAE7B,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,oBAAoB,CAA8B;gBAE9C,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,GAAG;IA8CpD,WAAW;IAwCjB,OAAO,CAAC,aAAa;YAUP,qBAAqB;IAU7B,kBAAkB,CAAC,SAAS,EAAE,GAAG;IAWvC,wBAAwB,CAAC,SAAS,EAAE,GAAG;IAWjC,yBAAyB;CAqHhC;AAED,eAAe,oBAAoB,CAAA"}
package/dist/platform.js CHANGED
@@ -87,7 +87,9 @@ const DEVICE_MATTER_CLUSTERS = {
87
87
  }, // Switch in HAP, RobotVacuumCleaner in Matter
88
88
  curtain: {
89
89
  windowCovering: {
90
- type: 4, // Drapery - safe default for LF conformance
90
+ // Use a lift-only/non-tilt covering type to satisfy Matter conformance rules.
91
+ // Drapery (4) can fail validation in some stacks when TL (tilt) is not present.
92
+ type: 0, // Rollershade
91
93
  },
92
94
  }, // WindowCovering → WindowCovering (includes curtain3, rollershade variants via normalization)
93
95
  blindtilt: {
@@ -120,12 +122,7 @@ const DEVICE_MATTER_CLUSTERS = {
120
122
  minLevel: 0,
121
123
  maxLevel: 254,
122
124
  },
123
- colorControl: {
124
- currentHue: 0,
125
- currentSaturation: 0,
126
- colorTemperatureMireds: 400,
127
- colorMode: 0,
128
- },
125
+ colorControl: {},
129
126
  }, // Lightbulb with color → OnOff + LevelControl + ColorControl
130
127
  lock: {
131
128
  doorLock: {
@@ -212,6 +209,63 @@ const DEVICE_MATTER_DEVICE_TYPE_KEYS = {
212
209
  meter: 'TemperatureSensor',
213
210
  waterdetector: 'LeakSensor',
214
211
  };
212
+ function normalizeTypeForMatter(typeValue) {
213
+ const raw = String(typeValue || '').trim().toLowerCase();
214
+ if (!raw) {
215
+ return 'unknown';
216
+ }
217
+ // Vacuum variants
218
+ if (['wosweeper', 'wosweepermini', 'wosweeperminipro', 'k10+', 'k10+ pro'].includes(raw)) {
219
+ return 'vacuum';
220
+ }
221
+ // Window covering variants
222
+ if (['blindtilt', 'blind tilt', 'curtain3', 'rollershade', 'roller shade', 'worollershade', 'wo rollershade'].includes(raw)) {
223
+ return 'curtain';
224
+ }
225
+ // Plug variants
226
+ if (['plug mini (jp)', 'plug mini (us)', 'plug mini (eu)'].includes(raw)) {
227
+ return 'plug';
228
+ }
229
+ // Meter variants
230
+ if (['meterplus', 'meter plus', 'meter plus (jp)', 'meterpro', 'meter pro', 'meterpro(co2)', 'meter pro (co2)'].includes(raw)) {
231
+ return 'meter';
232
+ }
233
+ // Relay switch variants
234
+ if (['relay switch 1', 'relay switch 1pm'].includes(raw)) {
235
+ return 'relay';
236
+ }
237
+ // Water detector variants
238
+ if (['water detector', 'waterdetector'].includes(raw)) {
239
+ return 'waterdetector';
240
+ }
241
+ // Fan variants
242
+ if (['smart fan', 'circulator fan', 'battery circulator fan', 'standing circulator fan'].includes(raw)) {
243
+ return 'fan';
244
+ }
245
+ // Light variants
246
+ if (['strip light', 'strip light 3', 'rgbic neon rope light', 'rgbic neon wire rope light', 'rgbicww floor lamp', 'rgbicww strip light'].includes(raw)) {
247
+ return 'lightstrip';
248
+ }
249
+ if (['color bulb', 'ceiling light', 'ceiling light pro', 'candle warmer lamp', 'floor lamp'].includes(raw)) {
250
+ return 'light';
251
+ }
252
+ // Sensor variants
253
+ if (raw === 'motion sensor') {
254
+ return 'motion';
255
+ }
256
+ if (['contact sensor', 'presence sensor'].includes(raw)) {
257
+ return 'contact';
258
+ }
259
+ // Lock variants
260
+ if (['smart lock', 'smart lock pro', 'smart lock ultra', 'lock lite', 'keypad', 'keypad touch', 'keypad vision', 'keypad vision pro', 'lock vision pro'].includes(raw)) {
261
+ return 'lock';
262
+ }
263
+ // Climate variant
264
+ if (raw === 'humidifier2') {
265
+ return 'humidifier';
266
+ }
267
+ return raw;
268
+ }
215
269
  // Factory function to create Matter handlers with Homebridge logger integration
216
270
  function createMatterHandlers(log, deviceId, type, client) {
217
271
  const lowerType = type.toLowerCase();
@@ -1026,40 +1080,7 @@ export class SwitchBotHAPPlatform {
1026
1080
  type: raw.configDeviceType ?? raw.type ?? raw.deviceType ?? 'unknown',
1027
1081
  _raw: raw,
1028
1082
  };
1029
- let type = d.type;
1030
- // Normalize device type variants for consistent Matter cluster lookup
1031
- // Vacuum variants
1032
- if (['wosweeper', 'wosweepermini', 'wosweeperminipro', 'k10+', 'k10+ pro'].includes((type || '').toLowerCase())) {
1033
- type = 'vacuum';
1034
- }
1035
- // Window covering variants
1036
- if (['blindtilt', 'blind tilt', 'curtain3', 'rollershade', 'roller shade', 'worollershade', 'wo rollershade'].includes((type || '').toLowerCase())) {
1037
- type = 'curtain';
1038
- }
1039
- // Plug variants
1040
- if (['plug mini (jp)', 'plug mini (us)'].includes((type || '').toLowerCase())) {
1041
- type = 'plug';
1042
- }
1043
- // Meter variants
1044
- if (['meterplus', 'meter plus (jp)', 'meterpro', 'meterpro(co2)'].includes((type || '').toLowerCase())) {
1045
- type = 'meter';
1046
- }
1047
- // Relay switch variants
1048
- if (['relay switch 1', 'relay switch 1pm'].includes((type || '').toLowerCase())) {
1049
- type = 'relay';
1050
- }
1051
- // Water detector variants
1052
- if (['water detector'].includes((type || '').toLowerCase())) {
1053
- type = 'waterdetector';
1054
- }
1055
- // Fan variants
1056
- if (['smart fan'].includes((type || '').toLowerCase())) {
1057
- type = 'fan';
1058
- }
1059
- // Light variants
1060
- if (['strip light'].includes((type || '').toLowerCase())) {
1061
- type = 'lightstrip';
1062
- }
1083
+ const type = normalizeTypeForMatter(d.type);
1063
1084
  const matterSupported = !!DEVICE_MATTER_SUPPORTED[(type || '').toLowerCase()];
1064
1085
  // Auto-detect Matter from Homebridge API, allow manual override via config
1065
1086
  const matterAvailable = !!(this.api?.isMatterAvailable?.() && this.api?.isMatterEnabled?.());
@@ -1312,40 +1333,7 @@ export class SwitchBotMatterPlatform {
1312
1333
  type: raw.configDeviceType ?? raw.type ?? raw.deviceType ?? 'unknown',
1313
1334
  _raw: raw,
1314
1335
  };
1315
- let type = d.type;
1316
- // Normalize device type variants for consistent Matter cluster lookup
1317
- // Vacuum variants
1318
- if (['wosweeper', 'wosweepermini', 'wosweeperminipro', 'k10+', 'k10+ pro'].includes((type || '').toLowerCase())) {
1319
- type = 'vacuum';
1320
- }
1321
- // Window covering variants
1322
- if (['blindtilt', 'blind tilt', 'curtain3', 'rollershade', 'roller shade', 'worollershade', 'wo rollershade'].includes((type || '').toLowerCase())) {
1323
- type = 'curtain';
1324
- }
1325
- // Plug variants
1326
- if (['plug mini (jp)', 'plug mini (us)'].includes((type || '').toLowerCase())) {
1327
- type = 'plug';
1328
- }
1329
- // Meter variants
1330
- if (['meterplus', 'meter plus (jp)', 'meterpro', 'meterpro(co2)'].includes((type || '').toLowerCase())) {
1331
- type = 'meter';
1332
- }
1333
- // Relay switch variants
1334
- if (['relay switch 1', 'relay switch 1pm'].includes((type || '').toLowerCase())) {
1335
- type = 'relay';
1336
- }
1337
- // Water detector variants
1338
- if (['water detector'].includes((type || '').toLowerCase())) {
1339
- type = 'waterdetector';
1340
- }
1341
- // Fan variants
1342
- if (['smart fan'].includes((type || '').toLowerCase())) {
1343
- type = 'fan';
1344
- }
1345
- // Light variants
1346
- if (['strip light'].includes((type || '').toLowerCase())) {
1347
- type = 'lightstrip';
1348
- }
1336
+ const type = normalizeTypeForMatter(d.type);
1349
1337
  const matterSupported = !!DEVICE_MATTER_SUPPORTED[(type || '').toLowerCase()];
1350
1338
  // Auto-detect Matter from Homebridge API, allow manual override via config
1351
1339
  const matterAvailable = this.api?.isMatterAvailable?.() && this.api?.isMatterEnabled?.();
@@ -1439,40 +1427,7 @@ export class SwitchBotMatterPlatform {
1439
1427
  if (!d.id) {
1440
1428
  continue;
1441
1429
  }
1442
- let type = 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
- }
1430
+ const type = normalizeTypeForMatter(d.type);
1476
1431
  const matterSupported = !!DEVICE_MATTER_SUPPORTED[(type || '').toLowerCase()];
1477
1432
  const useMatter = matterEnabled && matterSupported;
1478
1433
  if (!useMatter) {