@switchbot/homebridge-switchbot 5.0.0-beta.128 → 5.0.0-beta.129

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 (29) hide show
  1. package/config.schema.json +11 -0
  2. package/dist/devices/genericDevice.d.ts.map +1 -1
  3. package/dist/devices/genericDevice.js +55 -10
  4. package/dist/devices/genericDevice.js.map +1 -1
  5. package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -1
  6. package/dist/homebridge-ui/endpoints/discovery.js +10 -1
  7. package/dist/homebridge-ui/endpoints/discovery.js.map +1 -1
  8. package/dist/homebridge-ui/public/js/api.d.ts +4 -1
  9. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  10. package/dist/homebridge-ui/public/js/api.js +7 -1
  11. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  12. package/dist/homebridge-ui/public/js/api.ts +8 -1
  13. package/dist/homebridge-ui/public/js/app.js +149 -24
  14. package/dist/homebridge-ui/public/js/app.js.map +2 -2
  15. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
  16. package/dist/homebridge-ui/public/js/devices.js +1 -0
  17. package/dist/homebridge-ui/public/js/devices.js.map +1 -1
  18. package/dist/homebridge-ui/public/js/devices.ts +5 -2
  19. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
  20. package/dist/homebridge-ui/public/js/modals.js +149 -20
  21. package/dist/homebridge-ui/public/js/modals.js.map +1 -1
  22. package/dist/homebridge-ui/public/js/modals.ts +156 -26
  23. package/docs/variables/default.html +1 -1
  24. package/package.json +1 -1
  25. package/src/devices/genericDevice.ts +64 -10
  26. package/src/homebridge-ui/endpoints/discovery.ts +9 -1
  27. package/src/homebridge-ui/public/js/api.ts +8 -1
  28. package/src/homebridge-ui/public/js/devices.ts +5 -2
  29. package/src/homebridge-ui/public/js/modals.ts +156 -26
@@ -1 +1 @@
1
- <!DOCTYPE html><html class="default" lang="en" data-base="../"><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>default | @switchbot/homebridge-switchbot</title><meta name="description" content="Documentation for @switchbot/homebridge-switchbot"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script defer src="../assets/main.js"></script><script async src="../assets/icons.js" id="tsd-icons-script"></script><script async src="../assets/search.js" id="tsd-search-script"></script><script async src="../assets/navigation.js" id="tsd-nav-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => window.app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><a href="../index.html" class="title">@switchbot/homebridge-switchbot</a><div id="tsd-toolbar-links"></div><button id="tsd-search-trigger" class="tsd-widget" aria-label="Search"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-search"></use></svg></button><dialog id="tsd-search" aria-label="Search"><input role="combobox" id="tsd-search-input" aria-controls="tsd-search-results" aria-autocomplete="list" aria-expanded="true" autocapitalize="off" autocomplete="off" placeholder="Search the docs" maxLength="100"/><ul role="listbox" id="tsd-search-results"></ul><div id="tsd-search-status" aria-live="polite" aria-atomic="true"><div>Preparing search index...</div></div></dialog><a href="#" class="tsd-widget menu" id="tsd-toolbar-menu-trigger" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-menu"></use></svg></a></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb" aria-label="Breadcrumb"><li><a href="" aria-current="page">default</a></li></ul><h1>Variable default</h1></div><div class="tsd-signature"><span class="tsd-kind-variable">default</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol">(</span><span class="tsd-kind-parameter">api</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">API</span><span class="tsd-signature-symbol">)</span> <span class="tsd-signature-symbol">=&gt;</span> <span class="tsd-signature-type">void</span></div><div class="tsd-type-declaration"><h4>Type Declaration</h4><ul class="tsd-parameters"><li class="tsd-parameter-signature"><ul class="tsd-signatures"><li class="tsd-signature" id="__type"><span class="tsd-signature-symbol">(</span><span class="tsd-kind-parameter">api</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">API</span><span class="tsd-signature-symbol">)</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">void</span></li><li class="tsd-description"><div class="tsd-parameters"><h4 class="tsd-parameters-title">Parameters</h4><ul class="tsd-parameter-list"><li><span><span class="tsd-kind-parameter">api</span>: <span class="tsd-signature-type">API</span></span></li></ul></div><h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">void</span></h4></li></ul></li></ul></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/OpenWonderLabs/homebridge-switchbot/blob/912362b1a6962dafcc669931c2fbcb97af06d957/src/index.ts#L12">index.ts:12</a></li></ul></aside></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronDown"></use></svg><h3>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="../modules.html">@switchbot/homebridge-switchbot</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer></footer><div class="overlay"></div></body></html>
1
+ <!DOCTYPE html><html class="default" lang="en" data-base="../"><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>default | @switchbot/homebridge-switchbot</title><meta name="description" content="Documentation for @switchbot/homebridge-switchbot"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script defer src="../assets/main.js"></script><script async src="../assets/icons.js" id="tsd-icons-script"></script><script async src="../assets/search.js" id="tsd-search-script"></script><script async src="../assets/navigation.js" id="tsd-nav-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => window.app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><a href="../index.html" class="title">@switchbot/homebridge-switchbot</a><div id="tsd-toolbar-links"></div><button id="tsd-search-trigger" class="tsd-widget" aria-label="Search"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-search"></use></svg></button><dialog id="tsd-search" aria-label="Search"><input role="combobox" id="tsd-search-input" aria-controls="tsd-search-results" aria-autocomplete="list" aria-expanded="true" autocapitalize="off" autocomplete="off" placeholder="Search the docs" maxLength="100"/><ul role="listbox" id="tsd-search-results"></ul><div id="tsd-search-status" aria-live="polite" aria-atomic="true"><div>Preparing search index...</div></div></dialog><a href="#" class="tsd-widget menu" id="tsd-toolbar-menu-trigger" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-menu"></use></svg></a></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb" aria-label="Breadcrumb"><li><a href="" aria-current="page">default</a></li></ul><h1>Variable default</h1></div><div class="tsd-signature"><span class="tsd-kind-variable">default</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol">(</span><span class="tsd-kind-parameter">api</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">API</span><span class="tsd-signature-symbol">)</span> <span class="tsd-signature-symbol">=&gt;</span> <span class="tsd-signature-type">void</span></div><div class="tsd-type-declaration"><h4>Type Declaration</h4><ul class="tsd-parameters"><li class="tsd-parameter-signature"><ul class="tsd-signatures"><li class="tsd-signature" id="__type"><span class="tsd-signature-symbol">(</span><span class="tsd-kind-parameter">api</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">API</span><span class="tsd-signature-symbol">)</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">void</span></li><li class="tsd-description"><div class="tsd-parameters"><h4 class="tsd-parameters-title">Parameters</h4><ul class="tsd-parameter-list"><li><span><span class="tsd-kind-parameter">api</span>: <span class="tsd-signature-type">API</span></span></li></ul></div><h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">void</span></h4></li></ul></li></ul></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/OpenWonderLabs/homebridge-switchbot/blob/5f565afdb9bcc9ffeda470cd1b77f03cb155fff4/src/index.ts#L12">index.ts:12</a></li></ul></aside></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronDown"></use></svg><h3>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="../modules.html">@switchbot/homebridge-switchbot</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer></footer><div class="overlay"></div></body></html>
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@switchbot/homebridge-switchbot",
3
3
  "displayName": "SwitchBot",
4
4
  "type": "module",
5
- "version": "5.0.0-beta.128",
5
+ "version": "5.0.0-beta.129",
6
6
  "description": "The SwitchBot plugin allows you to access your SwitchBot device(s) from HomeKit.",
7
7
  "author": "SwitchBot <support@wondertechlabs.com> (https://github.com/SwitchBot)",
8
8
  "contributors": [
@@ -470,15 +470,70 @@ export class CurtainDevice extends GenericDevice {
470
470
  return undefined
471
471
  }
472
472
 
473
- // Try all combinations of (type, supportsLift, supportsTilt)
474
- const combos = [
475
- { matterType: 4, supportsLift: true, supportsTilt: false }, // Curtain
476
- { matterType: 0, supportsLift: true, supportsTilt: false }, // RollerShade
477
- { matterType: 8, supportsLift: false, supportsTilt: true }, // BlindTilt
478
- { matterType: 0, supportsLift: true, supportsTilt: true }, // Blind (lift+tilt)
479
- { matterType: 4, supportsLift: false, supportsTilt: false }, // Invalid, but try
480
- { matterType: 0, supportsLift: false, supportsTilt: false }, // Invalid, but try
481
- ]
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
+ return {
499
+ id: this.opts.id,
500
+ name: this.opts.name ?? this.opts.type,
501
+ protocol: 'matter',
502
+ clusters: [
503
+ {
504
+ type: 'WindowCovering',
505
+ clusterId: MATTER_CLUSTER_IDS.WindowCovering,
506
+ attributes,
507
+ },
508
+ ],
509
+ }
510
+ }
511
+
512
+ // Only try allowed types for each feature set (per Matter spec Table 8.2)
513
+ // - supportsLift && !supportsTilt: RollerShade (0), NOT Drapery (4)
514
+ // - !supportsLift && supportsTilt: BlindTilt (8)
515
+ // - supportsLift && supportsTilt: RollerShade (0) (or Blind, but 0 is safe)
516
+ // - supportsLift && supportsTilt && typeKey.includes('drapery'): allow Drapery (4)
517
+ // - !supportsLift && !supportsTilt: skip
518
+ interface MatterCombo {
519
+ matterType: number
520
+ supportsLift: boolean
521
+ supportsTilt: boolean
522
+ }
523
+ const combos: MatterCombo[] = []
524
+ if (supportsLift && supportsTilt) {
525
+ // Both supported: allow RollerShade (0), optionally Drapery (4) if typeKey includes 'drapery'
526
+ combos.push({ matterType: 0, supportsLift: true, supportsTilt: true })
527
+ if (typeKey.includes('drapery')) {
528
+ combos.push({ matterType: 4, supportsLift: true, supportsTilt: true })
529
+ }
530
+ } else if (supportsLift && !supportsTilt) {
531
+ // Only lift: allow RollerShade (0)
532
+ combos.push({ matterType: 0, supportsLift: true, supportsTilt: false })
533
+ } else if (!supportsLift && supportsTilt) {
534
+ // Only tilt: allow BlindTilt (8)
535
+ combos.push({ matterType: 8, supportsLift: false, supportsTilt: true })
536
+ }
482
537
  let lastError
483
538
  for (const combo of combos) {
484
539
  try {
@@ -1276,4 +1331,3 @@ export class WalletFinderDevice extends GenericDevice {}
1276
1331
  export class WoSweeperDevice extends VacuumDevice {}
1277
1332
  export class WoSweeperMiniDevice extends VacuumDevice {}
1278
1333
  export class WoSweeperMiniProDevice extends VacuumDevice {}
1279
- // Add missing closing braces for MeterDevice and WaterDetectorDevice
@@ -218,7 +218,15 @@ export function registerDiscoveryEndpoint(server: HomebridgePluginUiServer) {
218
218
  uiLog.info(`GET /discover - Total: ${allDiscovered.length} devices (BLE: ${bleCount}, OpenAPI: ${apiCount}, Both: ${bothCount})`)
219
219
  return { success: true, data: allDiscovered }
220
220
  } catch (e) {
221
- uiLog.error(`Error in /discover: ${e instanceof Error ? e.message : String(e)}`)
221
+ if (e instanceof Error) {
222
+ uiLog.error(`Error in /discover: ${e.message}`)
223
+ if (e.stack) {
224
+ uiLog.error(`[Stack] ${e.stack}`)
225
+ }
226
+ uiLog.error(`[Object]`, e)
227
+ } else {
228
+ uiLog.error(`Error in /discover: ${String(e)}`)
229
+ }
222
230
  throw new RequestError(`Failed to discover devices: ${e instanceof Error ? e.message : String(e)}`, e)
223
231
  }
224
232
  })
@@ -192,7 +192,7 @@ export async function addDevice(
192
192
  deviceId: string,
193
193
  name: string,
194
194
  type: string,
195
- options?: { address?: string, model?: string, rssi?: number, encryptionKey?: string, keyId?: string },
195
+ options?: { address?: string, model?: string, rssi?: number, encryptionKey?: string, keyId?: string, matterType?: number },
196
196
  ): Promise<any> {
197
197
  const payload: any = { deviceId, name, type }
198
198
  if (options?.address) {
@@ -210,6 +210,9 @@ export async function addDevice(
210
210
  if (options?.keyId) {
211
211
  payload.keyId = options.keyId
212
212
  }
213
+ if (options?.matterType !== undefined) {
214
+ payload.matterType = options.matterType
215
+ }
213
216
  uiLog.info('Adding device to config:', payload)
214
217
  const resp = await homebridge.request('/add-device', payload)
215
218
  uiLog.info('Add device response:', resp)
@@ -234,6 +237,7 @@ export async function updateDevice(
234
237
  deviceId: string,
235
238
  configDeviceName?: string,
236
239
  configDeviceType?: string,
240
+ options?: { matterType?: number },
237
241
  ): Promise<any> {
238
242
  const params: any = { deviceId }
239
243
  if (configDeviceName) {
@@ -242,6 +246,9 @@ export async function updateDevice(
242
246
  if (configDeviceType) {
243
247
  params.configDeviceType = configDeviceType
244
248
  }
249
+ if (options?.matterType !== undefined) {
250
+ params.matterType = options.matterType
251
+ }
245
252
  uiLog.info('[Update Device] Sending update request with params:', params)
246
253
  const resp = await homebridge.request('/update-device', params)
247
254
  uiLog.info('[Update Device] Update response:', resp)
@@ -1,4 +1,3 @@
1
-
2
1
  import { addDevice, fetchDevices, syncParentPluginConfigFromDisk } from './api.js'
3
2
  import { uiLog } from './logger.js'
4
3
  import { hideBusyUi, showBusyUi } from './modal.js'
@@ -11,6 +10,7 @@ type ImportDiscoveredDeviceResult = {
11
10
  address?: string
12
11
  encryptionKey?: string
13
12
  keyId?: string
13
+ matterType?: number
14
14
  } | null
15
15
 
16
16
  export async function addDeviceToConfig(device: any, options: { refresh?: boolean, showStatus?: boolean } = {}): Promise<{ added: boolean }> {
@@ -32,6 +32,7 @@ export async function addDeviceToConfig(device: any, options: { refresh?: boolea
32
32
  rssi: device.rssi,
33
33
  encryptionKey: importValues.encryptionKey,
34
34
  keyId: importValues.keyId,
35
+ matterType: importValues.matterType,
35
36
  })
36
37
  uiLog.info('Add device response:', resp)
37
38
 
@@ -97,7 +98,9 @@ export async function addDeviceToConfig(device: any, options: { refresh?: boolea
97
98
 
98
99
  export async function initRemoveAllButton(): Promise<void> {
99
100
  const removeAllBtn = document.getElementById('removeAllBtn')
100
- if (!removeAllBtn) { return }
101
+ if (!removeAllBtn) {
102
+ return
103
+ }
101
104
 
102
105
  removeAllBtn.addEventListener('click', async () => {
103
106
  const { deleteAllDevicesFromConfig } = await import('./devices-delete.js')
@@ -57,6 +57,7 @@ export async function importDiscoveredDevice(device: any): Promise<{ configDevic
57
57
  nameInput.style.fontSize = '14px'
58
58
  nameInput.style.boxSizing = 'border-box'
59
59
 
60
+ // --- Device Type Select ---
60
61
  const typeLabel = document.createElement('label')
61
62
  typeLabel.textContent = 'Config Device Type'
62
63
  typeLabel.style.display = 'block'
@@ -79,7 +80,6 @@ export async function importDiscoveredDevice(device: any): Promise<{ configDevic
79
80
  Object.keys(DEVICE_TYPES).forEach((categoryName) => {
80
81
  const optgroup = document.createElement('optgroup')
81
82
  optgroup.label = categoryName
82
-
83
83
  DEVICE_TYPES[categoryName as keyof typeof DEVICE_TYPES].forEach((deviceType) => {
84
84
  const opt = document.createElement('option')
85
85
  opt.value = deviceType
@@ -88,10 +88,69 @@ export async function importDiscoveredDevice(device: any): Promise<{ configDevic
88
88
  opt.selected = deviceType.toLowerCase() === detectedType
89
89
  optgroup.appendChild(opt)
90
90
  })
91
-
92
91
  typeSelect.appendChild(optgroup)
93
92
  })
94
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
+
95
154
  const macLabel = document.createElement('label')
96
155
  macLabel.textContent = 'BLE MAC Address (optional)'
97
156
  macLabel.style.display = 'block'
@@ -111,7 +170,6 @@ export async function importDiscoveredDevice(device: any): Promise<{ configDevic
111
170
  macInput.style.fontSize = '14px'
112
171
  macInput.style.boxSizing = 'border-box'
113
172
 
114
-
115
173
  // BLE Encryption Key
116
174
  const encryptionKeyLabel = document.createElement('label')
117
175
  encryptionKeyLabel.textContent = 'BLE Encryption Key (optional)'
@@ -173,7 +231,6 @@ export async function importDiscoveredDevice(device: any): Promise<{ configDevic
173
231
  importBtn.style.padding = '8px 20px'
174
232
  importBtn.style.fontSize = '13px'
175
233
 
176
-
177
234
  const cleanup = (result: { configDeviceName: string, configDeviceType: string, address?: string, encryptionKey?: string, keyId?: string } | null) => {
178
235
  div.remove()
179
236
  resolve(result)
@@ -186,6 +243,7 @@ export async function importDiscoveredDevice(device: any): Promise<{ configDevic
186
243
  address: macInput.value || undefined,
187
244
  encryptionKey: encryptionKeyInput.value || undefined,
188
245
  keyId: keyIdInput.value || undefined,
246
+ ...(windowCoveringTypes.includes(typeSelect.value) && matterTypeSelect.value !== '' ? { matterType: Number(matterTypeSelect.value) } : {}),
189
247
  })
190
248
 
191
249
  div.addEventListener('click', (event) => {
@@ -194,13 +252,14 @@ export async function importDiscoveredDevice(device: any): Promise<{ configDevic
194
252
  }
195
253
  })
196
254
 
197
-
198
255
  buttons.appendChild(cancelBtn)
199
256
  buttons.appendChild(importBtn)
200
257
  contentDiv.appendChild(nameLabel)
201
258
  contentDiv.appendChild(nameInput)
202
259
  contentDiv.appendChild(typeLabel)
203
260
  contentDiv.appendChild(typeSelect)
261
+ contentDiv.appendChild(matterTypeLabel)
262
+ contentDiv.appendChild(matterTypeSelect)
204
263
  contentDiv.appendChild(macLabel)
205
264
  contentDiv.appendChild(macInput)
206
265
  contentDiv.appendChild(encryptionKeyLabel)
@@ -218,6 +277,94 @@ export async function importDiscoveredDevice(device: any): Promise<{ configDevic
218
277
  }
219
278
 
220
279
  export async function editDevice(device: any): Promise<void> {
280
+ // --- Device Type Select ---
281
+ const typeLabel = document.createElement('label')
282
+ typeLabel.textContent = 'Config Device Type'
283
+ typeLabel.style.display = 'block'
284
+ typeLabel.style.marginBottom = '6px'
285
+ typeLabel.style.fontWeight = '500'
286
+ typeLabel.style.fontSize = '12px'
287
+ typeLabel.style.color = '#6b7280'
288
+
289
+ const typeSelect = document.createElement('select')
290
+ typeSelect.style.width = '100%'
291
+ typeSelect.style.padding = '8px 10px'
292
+ typeSelect.style.marginBottom = '12px'
293
+ typeSelect.style.borderRadius = '6px'
294
+ typeSelect.style.fontSize = '14px'
295
+ typeSelect.style.background = getComputedStyle(document.body).backgroundColor
296
+ typeSelect.style.color = getComputedStyle(document.body).color
297
+ typeSelect.style.border = '1px solid #ccc'
298
+ typeSelect.style.boxSizing = 'border-box'
299
+
300
+ // Add option groups with all API device types
301
+ Object.keys(DEVICE_TYPES).forEach((categoryName) => {
302
+ const optgroup = document.createElement('optgroup')
303
+ optgroup.label = categoryName
304
+ DEVICE_TYPES[categoryName as keyof typeof DEVICE_TYPES].forEach((deviceType) => {
305
+ const opt = document.createElement('option')
306
+ opt.value = deviceType
307
+ opt.text = deviceType
308
+ // Select current configDeviceType if set, otherwise match API deviceType
309
+ const currentType = device.configDeviceType || device.deviceType || device.type || ''
310
+ opt.selected = currentType === deviceType
311
+ optgroup.appendChild(opt)
312
+ })
313
+ typeSelect.appendChild(optgroup)
314
+ })
315
+
316
+ // --- Matter Type Advanced Field (only for window covering types) ---
317
+ const windowCoveringTypes = ['Roller Shade', 'Curtain', 'Curtain3', 'Blind Tilt']
318
+ const showMatterType = windowCoveringTypes.includes(device.configDeviceType || device.deviceType || device.type || '')
319
+ const matterTypeLabel = document.createElement('label')
320
+ matterTypeLabel.textContent = 'Matter Window Covering Type (Advanced)'
321
+ matterTypeLabel.style.display = showMatterType ? 'block' : 'none'
322
+ matterTypeLabel.style.marginBottom = '6px'
323
+ matterTypeLabel.style.fontWeight = '500'
324
+ matterTypeLabel.style.fontSize = '12px'
325
+ matterTypeLabel.style.color = '#6b7280'
326
+ matterTypeLabel.style.marginTop = '2px'
327
+ const matterTypes = [
328
+ { value: '', label: 'Auto-detect (recommended)' },
329
+ { value: 0, label: 'Roller Shade (0)' },
330
+ { value: 1, label: 'Roman Shade (1)' },
331
+ { value: 2, label: 'Balloon Shade (2)' },
332
+ { value: 3, label: 'Woven Wood (3)' },
333
+ { value: 4, label: 'Drapery (4)' },
334
+ { value: 5, label: 'Awning (5)' },
335
+ { value: 6, label: 'Shutter (6)' },
336
+ { value: 7, label: 'Tilt Blind (7)' },
337
+ { value: 8, label: 'Blind Tilt (8)' },
338
+ { value: 9, label: 'Projector Screen (9)' },
339
+ { value: 15, label: 'Other (15)' },
340
+ ]
341
+ const matterTypeSelect = document.createElement('select')
342
+ matterTypeSelect.style.width = '100%'
343
+ matterTypeSelect.style.marginBottom = '12px'
344
+ matterTypeSelect.style.padding = '8px 10px'
345
+ matterTypeSelect.style.borderRadius = '6px'
346
+ matterTypeSelect.style.fontSize = '14px'
347
+ matterTypeSelect.style.boxSizing = 'border-box'
348
+ matterTypes.forEach((opt) => {
349
+ const option = document.createElement('option')
350
+ option.value = String(opt.value)
351
+ option.textContent = opt.label
352
+ if (device.matterType != null && String(opt.value) === String(device.matterType)) {
353
+ option.selected = true
354
+ }
355
+ matterTypeSelect.appendChild(option)
356
+ })
357
+ function updateMatterTypeVisibility() {
358
+ if (windowCoveringTypes.includes(typeSelect.value)) {
359
+ matterTypeLabel.style.display = 'block'
360
+ matterTypeSelect.style.display = 'block'
361
+ } else {
362
+ matterTypeLabel.style.display = 'none'
363
+ matterTypeSelect.style.display = 'none'
364
+ }
365
+ }
366
+ typeSelect.addEventListener('change', updateMatterTypeVisibility)
367
+ updateMatterTypeVisibility()
221
368
  // Create modal dialog for editing
222
369
  const div = document.createElement('div')
223
370
  div.style.position = 'fixed'
@@ -298,26 +445,6 @@ export async function editDevice(device: any): Promise<void> {
298
445
  apiTypeInput.style.backgroundColor = '#f9fafb'
299
446
 
300
447
  // Editable config device type dropdown
301
- const typeLabel = document.createElement('label')
302
- typeLabel.textContent = 'Config Device Type (Override)'
303
- typeLabel.style.display = 'block'
304
- typeLabel.style.marginBottom = '6px'
305
- typeLabel.style.fontWeight = '500'
306
- typeLabel.style.fontSize = '12px'
307
- typeLabel.style.color = '#6b7280'
308
-
309
- const typeSelect = document.createElement('select')
310
- typeSelect.style.width = '100%'
311
- typeSelect.style.padding = '8px 10px'
312
- typeSelect.style.marginBottom = '12px'
313
- typeSelect.style.borderRadius = '6px'
314
- typeSelect.style.fontSize = '14px'
315
- typeSelect.style.background = getComputedStyle(nameInput).background
316
- typeSelect.style.color = getComputedStyle(nameInput).color
317
- typeSelect.style.border = getComputedStyle(nameInput).border
318
- typeSelect.style.boxSizing = 'border-box'
319
- typeSelect.style.cursor = 'pointer'
320
- typeSelect.style.transition = 'border-color 0.2s ease'
321
448
 
322
449
  // Add option groups with all API device types
323
450
  Object.keys(DEVICE_TYPES).forEach((categoryName) => {
@@ -378,9 +505,10 @@ export async function editDevice(device: any): Promise<void> {
378
505
  deviceId: device.id,
379
506
  configDeviceName: nameInput.value || undefined,
380
507
  configDeviceType: typeSelect.value,
508
+ matterType: windowCoveringTypes.includes(typeSelect.value) && matterTypeSelect.value !== '' ? Number(matterTypeSelect.value) : undefined,
381
509
  }
382
510
 
383
- await updateDevice(params.deviceId, params.configDeviceName, params.configDeviceType)
511
+ await updateDevice(params.deviceId, params.configDeviceName, params.configDeviceType, { matterType: params.matterType })
384
512
  await syncParentPluginConfigFromDisk()
385
513
 
386
514
  // Refresh device list
@@ -404,6 +532,8 @@ export async function editDevice(device: any): Promise<void> {
404
532
  contentDiv.appendChild(apiTypeInput)
405
533
  contentDiv.appendChild(typeLabel)
406
534
  contentDiv.appendChild(typeSelect)
535
+ contentDiv.appendChild(matterTypeLabel)
536
+ contentDiv.appendChild(matterTypeSelect)
407
537
  contentDiv.appendChild(errorMessage)
408
538
  contentDiv.appendChild(buttons)
409
539