@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.
- package/.github/copilot-instructions.md +39 -0
- package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/devices.js +10 -1
- package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
- package/dist/homebridge-ui/public/css/styles.css +111 -16
- package/dist/homebridge-ui/public/index.html +34 -60
- package/dist/homebridge-ui/public/js/api.d.ts +5 -1
- package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/api.js +13 -3
- package/dist/homebridge-ui/public/js/api.js.map +1 -1
- package/dist/homebridge-ui/public/js/api.ts +13 -3
- package/dist/homebridge-ui/public/js/app.js +338 -42
- package/dist/homebridge-ui/public/js/app.js.map +3 -3
- package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/devices.js +12 -3
- package/dist/homebridge-ui/public/js/devices.js.map +1 -1
- package/dist/homebridge-ui/public/js/devices.ts +13 -3
- package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.js +37 -3
- package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.ts +39 -2
- package/dist/homebridge-ui/public/js/modals.d.ts +5 -0
- package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/modals.js +146 -0
- package/dist/homebridge-ui/public/js/modals.js.map +1 -1
- package/dist/homebridge-ui/public/js/modals.ts +167 -0
- package/dist/homebridge-ui/public/js/render.d.ts +3 -1
- package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/render.js +135 -32
- package/dist/homebridge-ui/public/js/render.js.map +1 -1
- package/dist/homebridge-ui/public/js/render.ts +145 -32
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +64 -109
- package/dist/platform.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +2 -1
- package/dist/utils.js.map +1 -1
- package/docs/variables/default.html +1 -1
- package/package.json +9 -8
- package/src/homebridge-ui/endpoints/devices.ts +5 -2
- package/src/homebridge-ui/public/css/styles.css +111 -16
- package/src/homebridge-ui/public/index.html +34 -60
- package/src/homebridge-ui/public/js/api.ts +13 -3
- package/src/homebridge-ui/public/js/devices.ts +13 -3
- package/src/homebridge-ui/public/js/discovery.ts +39 -2
- package/src/homebridge-ui/public/js/modals.ts +167 -0
- package/src/homebridge-ui/public/js/render.ts +145 -32
- package/src/platform.ts +77 -109
- package/src/utils.ts +2 -1
- package/test/e2e/run-e2e.spec.ts +12 -10
- package/test/helpers/matter-harness.ts +2 -2
- package/test/matter-childbridge.spec.ts +5 -1
- package/test/matter-integration.spec.ts +41 -0
- package/test/utils.spec.ts +18 -2
- package/todo/COMPLETED.md +53 -0
- package/todo/TODO.md +13 -42
- 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
|
-
|
|
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.
|
|
172
|
-
li.style.
|
|
173
|
-
li.style.
|
|
174
|
-
li.style.
|
|
175
|
-
li.style.
|
|
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 = '
|
|
218
|
+
nameContainer.style.marginBottom = '0'
|
|
185
219
|
nameContainer.style.flexWrap = 'wrap'
|
|
186
|
-
nameContainer.style.gap = '
|
|
220
|
+
nameContainer.style.gap = '4px'
|
|
187
221
|
|
|
188
222
|
const name = document.createElement('div')
|
|
189
223
|
name.style.fontWeight = '500'
|
|
190
|
-
name.style.fontSize = '
|
|
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 = '
|
|
254
|
+
details.style.fontSize = '10px'
|
|
214
255
|
details.style.opacity = '0.7'
|
|
215
|
-
details.style.marginTop = '
|
|
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 = '
|
|
228
|
-
addBtn.style.marginLeft = '
|
|
229
|
-
addBtn.style.
|
|
230
|
-
addBtn.style.
|
|
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(
|
|
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.
|
|
426
|
-
nameContainer.style.
|
|
427
|
-
nameContainer.style.gap = '
|
|
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 = '
|
|
538
|
+
code.style.fontSize = '10px'
|
|
436
539
|
code.style.opacity = '0.75'
|
|
437
|
-
code.style.marginLeft = '
|
|
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 = '
|
|
451
|
-
meta.style.fontSize = '
|
|
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.
|
|
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 = '
|
|
468
|
-
editBtn.style.fontSize = '
|
|
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 = '
|
|
477
|
-
copyBtn.style.fontSize = '
|
|
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 = '
|
|
495
|
-
deleteBtn.style.fontSize = '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/test/e2e/run-e2e.spec.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
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('
|
|
8
|
+
describe('e2e scripts (manual-run harness)', () => {
|
|
9
9
|
if (!process.env.RUN_E2E) {
|
|
10
|
-
|
|
10
|
+
it.skip('e2e disabled - set RUN_E2E=true to enable', () => {})
|
|
11
11
|
return
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
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
|
|
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 =
|
|
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)
|
|
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: (
|
|
51
|
-
on: (
|
|
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
|
|
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()
|