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

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.
@@ -433,166 +433,31 @@ export class CurtainDevice extends GenericDevice {
433
433
 
434
434
  // Matter-specific descriptor for Curtain (Shade cluster) with conformance type
435
435
  async createMatterAccessory(api: any): Promise<any> {
436
- // Determine device type and features
437
- const typeKey = (this.opts.type || '').toLowerCase()
438
- // Dynamic detection: check config/state for lift/tilt support
439
- const hasConfigLift = !!this.opts.lift || typeof this.opts.lift === 'number'
440
- const hasConfigTilt = !!this.opts.tilt || typeof this.opts.tilt === 'number'
441
- let hasLiftAttrs = false
442
- let hasTiltAttrs = false
443
- if (typeof this.getState === 'function') {
444
- let state: any
445
- try {
446
- state = await this.getState()
447
- } catch (e) {
448
- state = undefined
449
- }
450
- if (state) {
451
- hasLiftAttrs = typeof state.position === 'number'
452
- hasTiltAttrs = typeof state.tilt === 'number'
453
- }
454
- }
455
- // For SwitchBot Curtain, always set supportsLift = true
456
- let supportsLift = hasConfigLift || hasLiftAttrs
457
- if (typeKey.includes('curtain')) {
458
- supportsLift = true
459
- } else if (!supportsLift && (typeKey.includes('rollershade') || typeKey.includes('blindtilt'))) {
460
- supportsLift = true
461
- }
462
- const supportsTilt = hasConfigTilt || hasTiltAttrs || typeKey.includes('blindtilt') || typeKey.includes('blind tilt')
463
-
464
- // Debug: Log feature detection variables
465
- this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} feature detection: typeKey='${typeKey}', hasConfigLift=${hasConfigLift}, hasLiftAttrs=${hasLiftAttrs}, hasConfigTilt=${hasConfigTilt}, hasTiltAttrs=${hasTiltAttrs}, supportsLift=${supportsLift}, supportsTilt=${supportsTilt}`)
466
-
467
- // If neither lift nor tilt is supported, do not register as Matter window covering
468
- if (!supportsLift && !supportsTilt) {
469
- this.log.warn(`[Matter] Skipping Matter registration for window covering device '${this.opts.name ?? this.opts.id}' (id: ${this.opts.id}): no lift or tilt support detected.`)
470
- return undefined
471
- }
472
-
473
- // Allow user to override Matter type via config (opts.matterType)
474
- if (typeof this.opts.matterType === 'number') {
475
- const matterType = this.opts.matterType
476
- this.log.info(`[Matter/Config] Using user-specified Matter type for ${this.opts.name ?? this.opts.id}: matterType=${matterType}`)
477
- // Build attributes for Matter WindowCovering
478
- const attributes: Record<string, any> = {
479
- type: {
480
- read: async () => {
481
- this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} returning config-overridden type attribute value:`, matterType)
482
- return matterType
483
- },
484
- write: undefined,
485
- },
486
- currentPosition: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: undefined },
487
- targetPosition: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: async (v: any) => this.setState({ position: Number(v) }) },
488
- }
489
- if (supportsTilt) {
490
- attributes.currentTilt = { read: async () => { const s = await this.getState(); return typeof s.tilt === 'number' ? s.tilt : 0 }, write: undefined }
491
- attributes.targetTilt = { read: async () => { const s = await this.getState(); return typeof s.tilt === 'number' ? s.tilt : 0 }, write: async (v: any) => this.setState({ tilt: Number(v) }) }
492
- }
493
- try {
494
- this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} attributes:`, JSON.stringify(attributes, null, 2))
495
- } catch (e) {
496
- this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} attributes: [unserializable]`)
497
- }
498
- const windowCoveringCluster = {
499
- type: 'WindowCovering', // for test compatibility
500
- matterType, // actual Matter type number
501
- clusterId: MATTER_CLUSTER_IDS.WindowCovering,
502
- attributes,
503
- }
504
- const clustersArr: any = [windowCoveringCluster]
505
- // Always set clusters.windowCovering to the WindowCovering cluster by clusterId
506
- const foundWC = clustersArr.find((c: any) => c && c.clusterId === MATTER_CLUSTER_IDS.WindowCovering)
507
- clustersArr.windowCovering = foundWC || null
508
- return {
509
- id: this.opts.id,
510
- name: this.opts.name ?? this.opts.type,
511
- protocol: 'matter',
512
- clusters: clustersArr,
513
- }
514
- }
515
-
516
- // Only try allowed types for each feature set (per Matter spec Table 8.2)
517
- // - supportsLift && !supportsTilt: RollerShade (0), NOT Drapery (4)
518
- // - !supportsLift && supportsTilt: BlindTilt (8)
519
- // - supportsLift && supportsTilt: RollerShade (0) (or Blind, but 0 is safe)
520
- // - supportsLift && supportsTilt && typeKey.includes('drapery'): allow Drapery (4)
521
- // - !supportsLift && !supportsTilt: skip
522
- interface MatterCombo {
523
- matterType: number
524
- supportsLift: boolean
525
- supportsTilt: boolean
436
+ // Always use the platform-provided Matter type (e.g., 5 for roller shade)
437
+ const matterType = 5
438
+ const attributes: Record<string, any> = {
439
+ type: {
440
+ read: async () => matterType,
441
+ write: undefined,
442
+ },
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) }) },
526
445
  }
527
- const combos: MatterCombo[] = []
528
- if (supportsLift && supportsTilt) {
529
- // Both supported: allow RollerShade (0), optionally Drapery (4) if typeKey includes 'drapery'
530
- combos.push({ matterType: 0, supportsLift: true, supportsTilt: true })
531
- if (typeKey.includes('drapery')) {
532
- combos.push({ matterType: 4, supportsLift: true, supportsTilt: true })
533
- }
534
- } else if (supportsLift && !supportsTilt) {
535
- // Only lift: for SwitchBot Curtain, use Drapery (4) for conformance
536
- if (typeKey.includes('curtain')) {
537
- combos.push({ matterType: 4, supportsLift: true, supportsTilt: false })
538
- } else {
539
- combos.push({ matterType: 0, supportsLift: true, supportsTilt: false })
540
- }
541
- } else if (!supportsLift && supportsTilt) {
542
- // Only tilt: allow BlindTilt (8)
543
- combos.push({ matterType: 8, supportsLift: false, supportsTilt: true })
446
+ const windowCoveringCluster = {
447
+ type: 'WindowCovering',
448
+ matterType,
449
+ clusterId: MATTER_CLUSTER_IDS.WindowCovering,
450
+ attributes,
544
451
  }
545
- let lastError
546
- for (const combo of combos) {
547
- try {
548
- this.log.info(`[Matter/Retry] Trying combo for ${this.opts.name ?? this.opts.id}: matterType=${combo.matterType}, supportsLift=${combo.supportsLift}, supportsTilt=${combo.supportsTilt}`)
549
- this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} trying combo: matterType=${combo.matterType}, supportsLift=${combo.supportsLift}, supportsTilt=${combo.supportsTilt}`)
550
- // Build attributes for Matter WindowCovering (string keys only)
551
- const attributes: Record<string, any> = {
552
- type: {
553
- read: async () => {
554
- this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} returning type attribute value:`, combo.matterType)
555
- return combo.matterType
556
- },
557
- write: undefined,
558
- },
559
- currentPosition: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: undefined },
560
- targetPosition: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: async (v: any) => this.setState({ position: Number(v) }) },
561
- }
562
- if (combo.supportsTilt) {
563
- attributes.currentTilt = { read: async () => { const s = await this.getState(); return typeof s.tilt === 'number' ? s.tilt : 0 }, write: undefined }
564
- attributes.targetTilt = { read: async () => { const s = await this.getState(); return typeof s.tilt === 'number' ? s.tilt : 0 }, write: async (v: any) => this.setState({ tilt: Number(v) }) }
565
- }
566
- // Log the final attributes object
567
- try {
568
- this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} attributes:`, JSON.stringify(attributes, null, 2))
569
- } catch (e) {
570
- this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} attributes: [unserializable]`)
571
- }
572
- // Try to register with this combo
573
- const windowCoveringCluster = {
574
- type: 'WindowCovering', // for test compatibility
575
- matterType: combo.matterType, // actual Matter type number
576
- clusterId: MATTER_CLUSTER_IDS.WindowCovering,
577
- attributes,
578
- }
579
- const clustersArr: any = [windowCoveringCluster]
580
- // Always set clusters.windowCovering to the WindowCovering cluster by clusterId
581
- const foundWC = clustersArr.find((c: any) => c && c.clusterId === MATTER_CLUSTER_IDS.WindowCovering)
582
- clustersArr.windowCovering = foundWC || null
583
- return {
584
- id: this.opts.id,
585
- name: this.opts.name ?? this.opts.type,
586
- protocol: 'matter',
587
- clusters: clustersArr,
588
- }
589
- } catch (err) {
590
- lastError = err
591
- this.log.warn(`[Matter/Retry] Failed combo matterType=${combo.matterType}, supportsLift=${combo.supportsLift}, supportsTilt=${combo.supportsTilt}: ${err}`)
592
- }
452
+ const clustersArr: any = [windowCoveringCluster]
453
+ const foundWC = clustersArr.find((c: any) => c && c.clusterId === MATTER_CLUSTER_IDS.WindowCovering)
454
+ clustersArr.windowCovering = foundWC || null
455
+ return {
456
+ id: this.opts.id,
457
+ name: this.opts.name ?? this.opts.type,
458
+ protocol: 'matter',
459
+ clusters: clustersArr,
593
460
  }
594
- // If all combos fail, throw last error
595
- throw lastError || new Error('All Matter window covering type combos failed')
596
461
  }
597
462
  }
598
463
 
@@ -91,66 +91,6 @@ export async function importDiscoveredDevice(device: any): Promise<{ configDevic
91
91
  typeSelect.appendChild(optgroup)
92
92
  })
93
93
 
94
- // --- Matter Type Advanced Field (only for window covering types) ---
95
-
96
- // Matter Type advanced field (only for window covering types)
97
- const windowCoveringTypes = ['Roller Shade', 'Curtain', 'Curtain3', 'Blind Tilt']
98
-
99
- const matterTypeLabel = document.createElement('label')
100
- matterTypeLabel.textContent = 'Matter Window Covering Type (Advanced)'
101
- matterTypeLabel.style.display = 'block'
102
- matterTypeLabel.style.marginBottom = '6px'
103
- matterTypeLabel.style.fontWeight = '500'
104
- matterTypeLabel.style.fontSize = '12px'
105
- matterTypeLabel.style.color = '#6b7280'
106
- matterTypeLabel.style.marginTop = '2px'
107
-
108
- // Table 8.2 from Matter spec (partial, most common types)
109
- const matterTypes = [
110
- { value: '', label: 'Auto-detect (recommended)' },
111
- { value: 0, label: 'Roller Shade (0)' },
112
- { value: 1, label: 'Roman Shade (1)' },
113
- { value: 2, label: 'Balloon Shade (2)' },
114
- { value: 3, label: 'Woven Wood (3)' },
115
- { value: 4, label: 'Drapery (4)' },
116
- { value: 5, label: 'Awning (5)' },
117
- { value: 6, label: 'Shutter (6)' },
118
- { value: 7, label: 'Tilt Blind (7)' },
119
- { value: 8, label: 'Blind Tilt (8)' },
120
- { value: 9, label: 'Projector Screen (9)' },
121
- { value: 15, label: 'Other (15)' },
122
- ]
123
-
124
- const matterTypeSelect = document.createElement('select')
125
- matterTypeSelect.style.width = '100%'
126
- matterTypeSelect.style.marginBottom = '12px'
127
- matterTypeSelect.style.padding = '8px 10px'
128
- matterTypeSelect.style.borderRadius = '6px'
129
- matterTypeSelect.style.fontSize = '14px'
130
- matterTypeSelect.style.boxSizing = 'border-box'
131
- matterTypes.forEach((opt) => {
132
- const option = document.createElement('option')
133
- option.value = String(opt.value)
134
- option.textContent = opt.label
135
- if (device.matterType != null && String(opt.value) === String(device.matterType)) {
136
- option.selected = true
137
- }
138
- matterTypeSelect.appendChild(option)
139
- })
140
-
141
- // Show/hide Matter Type field based on type
142
- function updateMatterTypeVisibility() {
143
- if (windowCoveringTypes.includes(typeSelect.value)) {
144
- matterTypeLabel.style.display = 'block'
145
- matterTypeSelect.style.display = 'block'
146
- } else {
147
- matterTypeLabel.style.display = 'none'
148
- matterTypeSelect.style.display = 'none'
149
- }
150
- }
151
- typeSelect.addEventListener('change', updateMatterTypeVisibility)
152
- updateMatterTypeVisibility()
153
-
154
94
  const macLabel = document.createElement('label')
155
95
  macLabel.textContent = 'BLE MAC Address (optional)'
156
96
  macLabel.style.display = 'block'
@@ -243,7 +183,6 @@ export async function importDiscoveredDevice(device: any): Promise<{ configDevic
243
183
  address: macInput.value || undefined,
244
184
  encryptionKey: encryptionKeyInput.value || undefined,
245
185
  keyId: keyIdInput.value || undefined,
246
- ...(windowCoveringTypes.includes(typeSelect.value) && matterTypeSelect.value !== '' ? { matterType: Number(matterTypeSelect.value) } : {}),
247
186
  })
248
187
 
249
188
  div.addEventListener('click', (event) => {
@@ -258,8 +197,6 @@ export async function importDiscoveredDevice(device: any): Promise<{ configDevic
258
197
  contentDiv.appendChild(nameInput)
259
198
  contentDiv.appendChild(typeLabel)
260
199
  contentDiv.appendChild(typeSelect)
261
- contentDiv.appendChild(matterTypeLabel)
262
- contentDiv.appendChild(matterTypeSelect)
263
200
  contentDiv.appendChild(macLabel)
264
201
  contentDiv.appendChild(macInput)
265
202
  contentDiv.appendChild(encryptionKeyLabel)
package/src/platform.ts CHANGED
@@ -96,9 +96,9 @@ export const DEVICE_MATTER_CLUSTERS: Record<string, any> = {
96
96
  },
97
97
  }, // Switch in HAP, RobotVacuumCleaner in Matter
98
98
  curtain: {
99
- /* windowCovering: {
100
- type: 0, // Generic - supports basic open/close/position features, but no tilt (curtain3/rollershade variants use this with normalization)
101
- }, */
99
+ windowCovering: {
100
+ type: 5, // GenericWindowCovering - supports lift feature (covers curtain3, rollershade variants via normalization)
101
+ },
102
102
  }, // WindowCovering → WindowCovering (includes curtain3, rollershade variants via normalization)
103
103
  blindtilt: {
104
104
  windowCovering: {