@switchbot/homebridge-switchbot 5.1.0-beta.8 → 5.1.0

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 (130) hide show
  1. package/.github/copilot-instructions.md +0 -63
  2. package/.github/scripts/normalize-changelog-header.cjs +1 -1
  3. package/.github/scripts/npm-version-script-esm-auto.js +1 -1
  4. package/.github/scripts/resolve-version-bump-esm.js +1 -1
  5. package/CHANGELOG.md +10 -9
  6. package/MIGRATION.md +0 -5
  7. package/README.md +0 -25
  8. package/config.schema.json +0 -11
  9. package/dist/{Platform.HAP.d.ts → SwitchBotHAPPlatform.d.ts} +16 -2
  10. package/dist/SwitchBotHAPPlatform.d.ts.map +1 -0
  11. package/dist/{Platform.HAP.js → SwitchBotHAPPlatform.js} +111 -40
  12. package/dist/SwitchBotHAPPlatform.js.map +1 -0
  13. package/dist/{Platform.Matter.d.ts → SwitchBotMatterPlatform.d.ts} +2 -4
  14. package/dist/SwitchBotMatterPlatform.d.ts.map +1 -0
  15. package/dist/{Platform.Matter.js → SwitchBotMatterPlatform.js} +6 -62
  16. package/dist/SwitchBotMatterPlatform.js.map +1 -0
  17. package/dist/deviceFactory.d.ts.map +1 -1
  18. package/dist/deviceFactory.js +5 -15
  19. package/dist/deviceFactory.js.map +1 -1
  20. package/dist/devices/genericDevice.d.ts +63 -5
  21. package/dist/devices/genericDevice.d.ts.map +1 -1
  22. package/dist/devices/genericDevice.js +251 -11
  23. package/dist/devices/genericDevice.js.map +1 -1
  24. package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -1
  25. package/dist/homebridge-ui/endpoints/config.js +2 -9
  26. package/dist/homebridge-ui/endpoints/config.js.map +1 -1
  27. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js +2 -9
  28. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js.map +1 -1
  29. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js +0 -8
  30. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js.map +1 -1
  31. package/dist/homebridge-ui/public/css/styles.css +1 -61
  32. package/dist/homebridge-ui/public/index.html +2 -61
  33. package/dist/homebridge-ui/public/js/advanced-settings.d.ts.map +1 -1
  34. package/dist/homebridge-ui/public/js/advanced-settings.js +3 -4
  35. package/dist/homebridge-ui/public/js/advanced-settings.js.map +1 -1
  36. package/dist/homebridge-ui/public/js/advanced-settings.ts +3 -4
  37. package/dist/homebridge-ui/public/js/api.d.ts +0 -16
  38. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  39. package/dist/homebridge-ui/public/js/api.js +0 -48
  40. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  41. package/dist/homebridge-ui/public/js/api.ts +1 -61
  42. package/dist/homebridge-ui/public/js/app.js +115 -267
  43. package/dist/homebridge-ui/public/js/app.js.map +4 -4
  44. package/dist/homebridge-ui/public/js/app.ts +4 -47
  45. package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -1
  46. package/dist/homebridge-ui/public/js/credentials.js +5 -11
  47. package/dist/homebridge-ui/public/js/credentials.js.map +1 -1
  48. package/dist/homebridge-ui/public/js/credentials.ts +5 -11
  49. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
  50. package/dist/homebridge-ui/public/js/devices.js +0 -6
  51. package/dist/homebridge-ui/public/js/devices.js.map +1 -1
  52. package/dist/homebridge-ui/public/js/devices.ts +0 -6
  53. package/dist/homebridge-ui/public/js/modals.d.ts +0 -1
  54. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
  55. package/dist/homebridge-ui/public/js/modals.js +0 -53
  56. package/dist/homebridge-ui/public/js/modals.js.map +1 -1
  57. package/dist/homebridge-ui/public/js/modals.ts +0 -58
  58. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
  59. package/dist/homebridge-ui/public/js/render.js +1 -2
  60. package/dist/homebridge-ui/public/js/render.js.map +1 -1
  61. package/dist/homebridge-ui/public/js/render.ts +1 -2
  62. package/dist/homebridge-ui/settings.js.map +1 -1
  63. package/dist/homebridge-ui/utils/config-parser.d.ts +0 -8
  64. package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -1
  65. package/dist/homebridge-ui/utils/config-parser.js +0 -8
  66. package/dist/homebridge-ui/utils/config-parser.js.map +1 -1
  67. package/dist/index.js +2 -2
  68. package/dist/index.js.map +1 -1
  69. package/dist/settings.d.ts +0 -11
  70. package/dist/settings.d.ts.map +1 -1
  71. package/dist/settings.js.map +1 -1
  72. package/dist/utils.d.ts +0 -5
  73. package/dist/utils.d.ts.map +1 -1
  74. package/dist/utils.js +0 -27
  75. package/dist/utils.js.map +1 -1
  76. package/docs/assets/highlight.css +15 -15
  77. package/docs/index.html +5 -22
  78. package/docs/variables/default.html +1 -1
  79. package/package.json +15 -9
  80. package/scripts/build-ui.js +0 -30
  81. package/src/{Platform.HAP.ts → SwitchBotHAPPlatform.ts} +112 -39
  82. package/src/{Platform.Matter.ts → SwitchBotMatterPlatform.ts} +8 -63
  83. package/src/deviceFactory.ts +5 -14
  84. package/src/devices/genericDevice.ts +271 -12
  85. package/src/homebridge-ui/endpoints/config.ts +2 -10
  86. package/src/homebridge-ui/public/css/styles.css +1 -61
  87. package/src/homebridge-ui/public/index.html +0 -59
  88. package/src/homebridge-ui/public/js/advanced-settings.ts +3 -4
  89. package/src/homebridge-ui/public/js/api.ts +1 -61
  90. package/src/homebridge-ui/public/js/app.ts +4 -47
  91. package/src/homebridge-ui/public/js/credentials.ts +5 -11
  92. package/src/homebridge-ui/public/js/devices.ts +0 -6
  93. package/src/homebridge-ui/public/js/modals.ts +0 -58
  94. package/src/homebridge-ui/public/js/render.ts +1 -2
  95. package/src/homebridge-ui/utils/config-parser.ts +0 -9
  96. package/src/index.ts +2 -2
  97. package/src/settings.ts +0 -12
  98. package/src/utils.ts +0 -31
  99. package/test/device/deviceFactory.spec.ts +9 -0
  100. package/test/device/getstate-cache.spec.ts +62 -0
  101. package/test/device/getstate-returns-status.spec.ts +89 -0
  102. package/test/device/water-detector-battery.spec.ts +81 -0
  103. package/test/device/water-detector-leak.spec.ts +43 -0
  104. package/test/hap-push-updates.spec.ts +150 -0
  105. package/test/platform/accessory-restore.spec.ts +2 -2
  106. package/test/platform/matter-childbridge.spec.ts +1 -1
  107. package/test/platform/matter-integration.spec.ts +1 -51
  108. package/test/platform/platform-edge.spec.ts +2 -2
  109. package/test/platform/platform.integration.spec.ts +1 -1
  110. package/test/utils/utils.spec.ts +0 -15
  111. package/.github/npm-version-script-esm.js +0 -97
  112. package/dist/Platform.HAP.d.ts.map +0 -1
  113. package/dist/Platform.HAP.js.map +0 -1
  114. package/dist/Platform.Matter.d.ts.map +0 -1
  115. package/dist/Platform.Matter.js.map +0 -1
  116. package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js +0 -23
  117. package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js.map +0 -1
  118. package/dist/homebridge-ui/public/js/legacy-view.d.ts +0 -3
  119. package/dist/homebridge-ui/public/js/legacy-view.d.ts.map +0 -1
  120. package/dist/homebridge-ui/public/js/legacy-view.js +0 -43
  121. package/dist/homebridge-ui/public/js/legacy-view.js.map +0 -1
  122. package/dist/homebridge-ui/public/js/legacy-view.ts +0 -99
  123. package/dist/homebridge-ui/utils/v4-detection.d.ts +0 -12
  124. package/dist/homebridge-ui/utils/v4-detection.d.ts.map +0 -1
  125. package/dist/homebridge-ui/utils/v4-detection.js +0 -23
  126. package/dist/homebridge-ui/utils/v4-detection.js.map +0 -1
  127. package/src/homebridge-ui/public/js/legacy-view.js +0 -43
  128. package/src/homebridge-ui/public/js/legacy-view.ts +0 -99
  129. package/src/homebridge-ui/utils/v4-detection.ts +0 -23
  130. package/test/homebridge-ui/config-parser.spec.ts +0 -61
@@ -9,57 +9,8 @@
9
9
  </head>
10
10
 
11
11
  <body>
12
-
13
12
  <h1>🤖 SwitchBot Configuration</h1>
14
13
 
15
- <!-- ...existing code... -->
16
-
17
- <!-- Centered legacy view button at the bottom -->
18
- <div class="centered-toggle-btn">
19
- <button id="legacyViewBtn">Legacy View</button>
20
- <button id="dynamicViewBtn" style="display: none;">Dynamic View</button>
21
- </div>
22
- <style>
23
- .centered-toggle-btn {
24
- display: flex;
25
- flex-direction: row;
26
- justify-content: center;
27
- align-items: center;
28
- margin-top: 32px;
29
- margin-bottom: 12px;
30
- gap: 12px;
31
- }
32
- #legacyViewBtn, #dynamicViewBtn {
33
- font-size: 12px;
34
- padding: 4px 10px;
35
- }
36
- </style>
37
-
38
- <div id="dynamicUiContainer">
39
-
40
- <!-- v4 config incompatibility warning – shown dynamically by app.js -->
41
- <div id="v4ConfigWarning" class="v4-config-warning" style="display: none">
42
- <strong>⚠️ Incompatible v4 Configuration Detected</strong>
43
- <p>
44
- Your config.json appears to be in the <strong>v4 format</strong> (using <code>credentials</code> and
45
- <code>options.devices</code> fields). This format is <strong>not compatible with v5 or newer</strong> of the
46
- SwitchBot plugin. Devices will not be recognised until the config is updated.
47
- </p>
48
- <p>
49
- Please update your config to the v5 format:
50
- </p>
51
- <ul>
52
- <li>Move <code>credentials.token</code> → <code>openApiToken</code> (top level)</li>
53
- <li>Move <code>credentials.secret</code> → <code>openApiSecret</code> (top level)</li>
54
- <li>Move <code>options.devices</code> → <code>devices</code> (top level)</li>
55
- <li>Rename per-device <code>connectionType</code> → <code>connectionPreference</code></li>
56
- <li>Remove the <code>credentials</code> and <code>options</code> wrapper objects</li>
57
- </ul>
58
- <p>
59
- See the <a href="https://github.com/OpenWonderLabs/homebridge-switchbot/blob/latest/MIGRATION.md" target="_blank" rel="noopener">Migration Guide</a> for full details.
60
- </p>
61
- </div>
62
-
63
14
  <div class="card compact">
64
15
  <h2>API Credentials</h2>
65
16
  <div class="credentials-row">
@@ -253,18 +204,8 @@
253
204
  </form>
254
205
  </div>
255
206
 
256
- </div> <!-- end dynamicUiContainer -->
257
-
258
- <div id="legacyViewContainer" style="display: none;">
259
- <div class="card compact">
260
- <h2>Legacy Config View</h2>
261
- <div id="legacyConfigForm"></div>
262
- </div>
263
- </div>
264
-
265
207
  <script type="module" src="js/app.js"></script>
266
208
  <script type="module" src="js/advanced-settings.js"></script>
267
- <script type="module" src="js/legacy-view.js"></script>
268
209
  </body>
269
210
 
270
211
  </html>
@@ -46,11 +46,10 @@ async function saveAdvancedSettings(): Promise<void> {
46
46
  if (!Array.isArray(pluginConfigBlocks) || !pluginConfigBlocks.length) {
47
47
  throw new Error('No plugin config blocks returned from Homebridge')
48
48
  }
49
- // Find or add the SwitchBot config block
50
- let idx = pluginConfigBlocks.findIndex(c => (c.platform || c.name || '').toLowerCase().includes('switchbot'))
49
+ // Find the SwitchBot config block
50
+ const idx = pluginConfigBlocks.findIndex(c => (c.platform || c.name || '').toLowerCase().includes('switchbot'))
51
51
  if (idx === -1) {
52
- pluginConfigBlocks.push({ platform: 'SwitchBot' })
53
- idx = pluginConfigBlocks.length - 1
52
+ throw new Error('SwitchBot config not found')
54
53
  }
55
54
  const config = pluginConfigBlocks[idx]
56
55
  // Update config values from UI
@@ -1,6 +1,5 @@
1
1
  // Fetch the list of configured devices from the Homebridge UI API
2
2
  import { isValidDeviceType, normalizeDeviceType } from '../../../device-types.js'
3
- import { isV4Config } from '../../utils/v4-detection.js'
4
3
  import './types.js'
5
4
  import { uiLog } from './logger.js'
6
5
  import { toastError } from './toast.js'
@@ -23,35 +22,6 @@ export async function fetchDevices(): Promise<any[]> {
23
22
  }
24
23
  }
25
24
 
26
- /**
27
- * Detect whether the current plugin config is in the legacy v4 format.
28
- *
29
- * v4 configs stored credentials under a `credentials` sub-object and devices
30
- * under `options.devices`. v5+ flattened these to `openApiToken`,
31
- * `openApiSecret`, and a root-level `devices` array.
32
- *
33
- * Returns true when a v4 config block is found, false otherwise.
34
- */
35
- export async function detectV4Config(): Promise<boolean> {
36
- try {
37
- if (typeof homebridge.getPluginConfig !== 'function') {
38
- return false
39
- }
40
- const configArr = await homebridge.getPluginConfig()
41
- if (!Array.isArray(configArr)) {
42
- return false
43
- }
44
- const config = configArr.find(isSwitchBotPlatformConfig)
45
- if (!config || typeof config !== 'object') {
46
- return false
47
- }
48
- return isV4Config(config)
49
- } catch (e) {
50
- uiLog.warn('Error detecting v4 config:', e)
51
- return false
52
- }
53
- }
54
-
55
25
  /**
56
26
  * Validate and auto-correct device types in the config array before saving.
57
27
  * Returns an array of errors for devices that cannot be fixed.
@@ -219,19 +189,7 @@ export async function addDevice(
219
189
  deviceId: string,
220
190
  name: string,
221
191
  type: string,
222
- options?: {
223
- address?: string
224
- model?: string
225
- rssi?: number
226
- connectionPreference?: string
227
- externalPublishProtocol?: string
228
- room?: string
229
- encryptionKey?: string
230
- keyId?: string
231
- refreshRate?: number
232
- blePollingEnabled?: boolean
233
- blePollIntervalMs?: number
234
- },
192
+ options?: { address?: string, model?: string, rssi?: number, encryptionKey?: string, keyId?: string },
235
193
  ): Promise<any> {
236
194
  if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') {
237
195
  throw new TypeError('Homebridge UI API not available')
@@ -266,24 +224,6 @@ export async function addDevice(
266
224
  if (options?.keyId) {
267
225
  newDevice.keyId = options.keyId
268
226
  }
269
- if (options?.connectionPreference) {
270
- newDevice.connectionPreference = options.connectionPreference
271
- }
272
- if (options?.externalPublishProtocol) {
273
- newDevice.externalPublishProtocol = options.externalPublishProtocol
274
- }
275
- if (options?.room) {
276
- newDevice.room = options.room
277
- }
278
- if (typeof options?.refreshRate === 'number' && Number.isFinite(options.refreshRate)) {
279
- newDevice.refreshRate = options.refreshRate
280
- }
281
- if (typeof options?.blePollingEnabled === 'boolean') {
282
- newDevice.blePollingEnabled = options.blePollingEnabled
283
- }
284
- if (typeof options?.blePollIntervalMs === 'number' && Number.isFinite(options.blePollIntervalMs)) {
285
- newDevice.blePollIntervalMs = options.blePollIntervalMs
286
- }
287
227
  config.devices.push(newDevice)
288
228
  await homebridge.updatePluginConfig(configArr)
289
229
  if (typeof homebridge.savePluginConfig === 'function') {
@@ -1,56 +1,13 @@
1
- import { detectV4Config } from './api.js'
2
1
  import { loadCredentialStatus, saveCredentials } from './credentials.js'
3
2
  import { initRemoveAllButton, loadConfiguredDevices } from './devices.js'
4
- import { discoverDevices, initializeDiscoverySettings } from './discovery.js'
3
+ import { discoverDevices, initializeDiscoverySettings } from './discovery.js';
5
4
 
6
- // Ensure SwitchBot config block exists before any UI actions
7
- async function ensureSwitchBotConfigBlock(): Promise<void> {
8
- try {
9
- if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') {
10
- return
11
- }
12
- let configArr = await homebridge.getPluginConfig()
13
- if (!Array.isArray(configArr)) {
14
- configArr = []
15
- }
16
- const idx = configArr.findIndex(c => (c.platform || c.name || '').toLowerCase().includes('switchbot'))
17
- if (idx === -1) {
18
- configArr.push({ platform: 'SwitchBot' })
19
- await homebridge.updatePluginConfig(configArr)
20
- if (typeof homebridge.savePluginConfig === 'function') {
21
- await homebridge.savePluginConfig()
22
- }
23
- }
24
- } catch (_e) {
25
- // Ignore errors
26
- }
27
- }
28
-
29
- ;
30
-
31
- (window as any).loadCredentialStatus = loadCredentialStatus
32
- ;(window as any).saveCredentials = saveCredentials
33
- ;(window as any).discoverDevices = discoverDevices
34
-
35
- /**
36
- * Show or hide the v4 config incompatibility warning banner.
37
- */
38
- async function checkV4Config(): Promise<void> {
39
- const banner = document.getElementById('v4ConfigWarning')
40
- if (!banner) {
41
- return
42
- }
43
- const isV4 = await detectV4Config()
44
- if (isV4) {
45
- banner.style.display = 'block'
46
- }
47
- }
5
+ (window as any).loadCredentialStatus = loadCredentialStatus;
6
+ (window as any).saveCredentials = saveCredentials;
7
+ (window as any).discoverDevices = discoverDevices
48
8
 
49
9
  // Initialize on page load
50
-
51
10
  async function init(): Promise<void> {
52
- await ensureSwitchBotConfigBlock()
53
- await checkV4Config()
54
11
  await loadCredentialStatus()
55
12
  await initializeDiscoverySettings()
56
13
  await loadConfiguredDevices()
@@ -63,20 +63,14 @@ export async function saveCredentials(): Promise<void> {
63
63
  if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') {
64
64
  throw new TypeError('Homebridge UI API not available')
65
65
  }
66
- let configArr = await homebridge.getPluginConfig()
67
- if (!Array.isArray(configArr)) {
68
- configArr = []
69
- }
70
- let idx = configArr.findIndex(c => (c.platform || c.name || '').toLowerCase() === 'switchbot')
71
- if (idx === -1) {
72
- // Add new SwitchBot platform config
73
- configArr.push({ platform: 'SwitchBot' })
74
- idx = configArr.length - 1
66
+ const configArr = await homebridge.getPluginConfig()
67
+ if (!Array.isArray(configArr) || configArr.length === 0) {
68
+ throw new Error('No plugin config found')
75
69
  }
76
- const config = configArr[idx]
70
+ const config = configArr[0]
77
71
  config.openApiToken = token
78
72
  config.openApiSecret = secret
79
- await homebridge.updatePluginConfig(configArr)
73
+ await homebridge.updatePluginConfig([config])
80
74
  if (typeof homebridge.savePluginConfig === 'function') {
81
75
  await homebridge.savePluginConfig()
82
76
  }
@@ -26,14 +26,8 @@ export async function addDeviceToConfig(device: any, options: { refresh?: boolea
26
26
  address: importValues.address,
27
27
  model: device.model,
28
28
  rssi: device.rssi,
29
- connectionPreference: importValues.connectionPreference,
30
- externalPublishProtocol: importValues.externalPublishProtocol,
31
- room: importValues.room,
32
29
  encryptionKey: importValues.encryptionKey,
33
30
  keyId: importValues.keyId,
34
- refreshRate: importValues.refreshRate,
35
- blePollingEnabled: importValues.blePollingEnabled,
36
- blePollIntervalMs: importValues.blePollIntervalMs,
37
31
  })
38
32
  uiLog.info('Add device response:', resp)
39
33
 
@@ -6,7 +6,6 @@ export type ImportDiscoveredDeviceResult = {
6
6
  configDeviceType: string
7
7
  address?: string
8
8
  connectionPreference?: string
9
- externalPublishProtocol?: string
10
9
  room?: string
11
10
  encryptionKey?: string
12
11
  keyId?: string
@@ -155,30 +154,6 @@ export async function importDiscoveredDevice(device: any): Promise<ImportDiscove
155
154
  connectionPrefSelect.appendChild(opt)
156
155
  })
157
156
 
158
- // --- External Publish Protocol ---
159
- const externalPublishLabel = document.createElement('label')
160
- externalPublishLabel.textContent = 'Publish as External Device'
161
- externalPublishLabel.style.display = 'block'
162
- externalPublishLabel.style.marginBottom = '6px'
163
- externalPublishLabel.style.fontWeight = '500'
164
- externalPublishLabel.style.fontSize = '12px'
165
- externalPublishLabel.style.color = '#6b7280'
166
-
167
- const externalPublishSelect = document.createElement('select')
168
- externalPublishSelect.style.width = '100%'
169
- externalPublishSelect.style.marginBottom = '12px'
170
- externalPublishSelect.style.padding = '8px 10px'
171
- externalPublishSelect.style.borderRadius = '6px'
172
- externalPublishSelect.style.fontSize = '14px'
173
- externalPublishSelect.style.boxSizing = 'border-box'
174
- ;['none', 'hap', 'matter'].forEach((val) => {
175
- const opt = document.createElement('option')
176
- opt.value = val
177
- opt.text = val === 'none' ? 'None' : val.toUpperCase()
178
- opt.selected = (device.externalPublishProtocol || 'none') === val
179
- externalPublishSelect.appendChild(opt)
180
- })
181
-
182
157
  // --- Room ---
183
158
  const roomLabel = document.createElement('label')
184
159
  roomLabel.textContent = 'Room'
@@ -333,7 +308,6 @@ export async function importDiscoveredDevice(device: any): Promise<ImportDiscove
333
308
  configDeviceType: typeSelect.value || device.type,
334
309
  address: macInput.value || undefined,
335
310
  connectionPreference: connectionPrefSelect.value || undefined,
336
- externalPublishProtocol: externalPublishSelect.value || 'none',
337
311
  room: roomInput.value || undefined,
338
312
  encryptionKey: encryptionKeyInput.value || undefined,
339
313
  keyId: keyIdInput.value || undefined,
@@ -357,8 +331,6 @@ export async function importDiscoveredDevice(device: any): Promise<ImportDiscove
357
331
  contentDiv.appendChild(typeSelect)
358
332
  contentDiv.appendChild(connectionPrefLabel)
359
333
  contentDiv.appendChild(connectionPrefSelect)
360
- contentDiv.appendChild(externalPublishLabel)
361
- contentDiv.appendChild(externalPublishSelect)
362
334
  contentDiv.appendChild(roomLabel)
363
335
  contentDiv.appendChild(roomInput)
364
336
  contentDiv.appendChild(macLabel)
@@ -601,30 +573,6 @@ export async function editDevice(device: any): Promise<void> {
601
573
  connectionPrefSelect.appendChild(opt)
602
574
  })
603
575
 
604
- // --- External Publish Protocol ---
605
- const externalPublishLabel = document.createElement('label')
606
- externalPublishLabel.textContent = 'Publish as External Device'
607
- externalPublishLabel.style.display = 'block'
608
- externalPublishLabel.style.marginBottom = '6px'
609
- externalPublishLabel.style.fontWeight = '500'
610
- externalPublishLabel.style.fontSize = '12px'
611
- externalPublishLabel.style.color = '#6b7280'
612
-
613
- const externalPublishSelect = document.createElement('select')
614
- externalPublishSelect.style.width = '100%'
615
- externalPublishSelect.style.marginBottom = '12px'
616
- externalPublishSelect.style.padding = '8px 10px'
617
- externalPublishSelect.style.borderRadius = '6px'
618
- externalPublishSelect.style.fontSize = '14px'
619
- externalPublishSelect.style.boxSizing = 'border-box'
620
- ;['none', 'hap', 'matter'].forEach((val) => {
621
- const opt = document.createElement('option')
622
- opt.value = val
623
- opt.text = val === 'none' ? 'None' : val.toUpperCase()
624
- opt.selected = (device.externalPublishProtocol || 'none') === val
625
- externalPublishSelect.appendChild(opt)
626
- })
627
-
628
576
  // --- Room ---
629
577
  const roomLabel = document.createElement('label')
630
578
  roomLabel.textContent = 'Room'
@@ -726,7 +674,6 @@ export async function editDevice(device: any): Promise<void> {
726
674
  configDeviceName: nameInput.value || undefined,
727
675
  configDeviceType: typeSelect.value,
728
676
  connectionPreference: connectionPrefSelect.value,
729
- externalPublishProtocol: externalPublishSelect.value,
730
677
  room: roomInput.value || undefined,
731
678
  encryptionKey: encryptionKeyInput.value || undefined,
732
679
  keyId: keyIdInput.value || undefined,
@@ -743,9 +690,6 @@ export async function editDevice(device: any): Promise<void> {
743
690
  if (params.room !== undefined) {
744
691
  options.room = params.room
745
692
  }
746
- if (params.externalPublishProtocol !== undefined) {
747
- options.externalPublishProtocol = params.externalPublishProtocol
748
- }
749
693
  if (params.encryptionKey !== undefined) {
750
694
  options.encryptionKey = params.encryptionKey
751
695
  }
@@ -795,8 +739,6 @@ export async function editDevice(device: any): Promise<void> {
795
739
  contentDiv.appendChild(typeSelect)
796
740
  contentDiv.appendChild(connectionPrefLabel)
797
741
  contentDiv.appendChild(connectionPrefSelect)
798
- contentDiv.appendChild(externalPublishLabel)
799
- contentDiv.appendChild(externalPublishSelect)
800
742
  contentDiv.appendChild(roomLabel)
801
743
  contentDiv.appendChild(roomInput)
802
744
  contentDiv.appendChild(encryptionKeyLabel)
@@ -1014,9 +1014,8 @@ export function renderDeviceList(list: any[]): void {
1014
1014
  const id = `ID: ${deviceIdentifier}`
1015
1015
  const typeText = d.configDeviceType || d.type ? `Type: ${d.configDeviceType || d.type}` : ''
1016
1016
  const connText = d.connectionPreference ? `Conn: ${d.connectionPreference}` : ''
1017
- const extText = d.externalPublishProtocol && d.externalPublishProtocol !== 'none' ? `external: ${d.externalPublishProtocol}` : ''
1018
1017
  const roomText = d.room ? `Room: ${d.room}` : ''
1019
- meta.textContent = [id, typeText, connText, extText, roomText].filter(Boolean).join(' | ')
1018
+ meta.textContent = [id, typeText, connText, roomText].filter(Boolean).join(' | ')
1020
1019
 
1021
1020
  info.appendChild(nameContainer)
1022
1021
  info.appendChild(meta)
@@ -57,15 +57,6 @@ export function getAllDevices(platform: any): any[] {
57
57
  })
58
58
  }
59
59
 
60
- /**
61
- * Detect whether a platform config block is in the legacy v4 format.
62
- *
63
- * v4 configs stored credentials under a `credentials` sub-object and devices
64
- * under `options.devices`. v5+ flattened these to `openApiToken`,
65
- * `openApiSecret`, and a root-level `devices` array.
66
- */
67
- export { isV4Config } from './v4-detection.js'
68
-
69
60
  /**
70
61
  * Get credential from platform config
71
62
  */
package/src/index.ts CHANGED
@@ -4,9 +4,9 @@
4
4
  */
5
5
  import type { API } from 'homebridge'
6
6
 
7
- import { SwitchBotHAPPlatform } from './Platform.HAP.js'
8
- import { SwitchBotMatterPlatform } from './Platform.Matter.js'
9
7
  import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'
8
+ import { SwitchBotHAPPlatform } from './SwitchBotHAPPlatform.js'
9
+ import { SwitchBotMatterPlatform } from './SwitchBotMatterPlatform.js'
10
10
  import { createPlatformProxy } from './utils.js'
11
11
 
12
12
  /**
package/src/settings.ts CHANGED
@@ -1,24 +1,12 @@
1
1
  export const PLUGIN_NAME = '@switchbot/homebridge-switchbot'
2
2
  export const PLATFORM_NAME = 'SwitchBot'
3
3
 
4
- export interface SwitchBotDeviceConfig {
5
- deviceId?: string
6
- id?: string
7
- configDeviceName?: string
8
- name?: string
9
- configDeviceType?: string
10
- type?: string
11
- externalPublishProtocol?: 'none' | 'hap' | 'matter'
12
- [key: string]: any
13
- }
14
-
15
4
  export interface SwitchBotPluginConfig {
16
5
  openApiToken?: string
17
6
  openApiSecret?: string
18
7
  preferMatter?: boolean
19
8
  enableMatter?: boolean
20
9
  enableBLE?: boolean // Enable or disable BLE support
21
- devices?: SwitchBotDeviceConfig[]
22
10
  // other plugin-specific configuration
23
11
  [key: string]: any
24
12
  }
package/src/utils.ts CHANGED
@@ -1137,37 +1137,6 @@ export function normalizeConfig(raw?: PlatformConfig): SwitchBotPluginConfig {
1137
1137
  return { ...(raw as any) } as SwitchBotPluginConfig
1138
1138
  }
1139
1139
 
1140
- /**
1141
- * Resolve the per-device external publish protocol from raw config.
1142
- * Accepts canonical values (`none`, `hap`, `matter`) and common aliases.
1143
- */
1144
- export function resolveExternalPublishProtocol(rawDevice: any): 'none' | 'hap' | 'matter' {
1145
- const raw = String(
1146
- rawDevice?.externalPublishProtocol
1147
- ?? rawDevice?.externalDeviceProtocol
1148
- ?? rawDevice?.publishExternal
1149
- ?? rawDevice?.external
1150
- ?? 'none',
1151
- ).trim().toLowerCase()
1152
-
1153
- if (raw === 'true') {
1154
- return 'hap'
1155
- }
1156
- if (raw === 'false') {
1157
- return 'none'
1158
- }
1159
- if (raw === 'homekit') {
1160
- return 'hap'
1161
- }
1162
- if (raw === 'none' || raw === 'off' || raw === 'disabled') {
1163
- return 'none'
1164
- }
1165
- if (raw === 'hap' || raw === 'matter') {
1166
- return raw
1167
- }
1168
- return 'none'
1169
- }
1170
-
1171
1140
  // Create a Proxy constructor that instantiates the right platform implementation at runtime.
1172
1141
  /**
1173
1142
  * Creates a proxy class that instantiates the correct platform implementation (HAP or Matter) at runtime.
@@ -30,4 +30,13 @@ describe('createDevice', () => {
30
30
  const result = await createDevice({ id: 'xyz', type: 'UnknownType' }, dummyConfig as any, false)
31
31
  expect(result.instance.constructor.name).toBe('GenericDevice')
32
32
  })
33
+
34
+ it('should create a WaterDetectorDevice for normalized "waterdetector" type', async () => {
35
+ // normalizeTypeForMatter() returns 'waterdetector' (no space) for water detector variants.
36
+ // This regression test ensures the no-space key is mapped so devices aren't silently
37
+ // downgraded to GenericDevice (which would expose a bogus Switch service).
38
+ const result = await createDevice({ id: 'wd1', type: 'waterdetector' }, dummyConfig as any, false)
39
+ expect(result.instance).toBeDefined()
40
+ expect(result.instance.constructor.name).toBe('WaterDetectorDevice')
41
+ })
33
42
  })
@@ -0,0 +1,62 @@
1
+ /* Copyright(C) 2021-2026, donavanbecker (https://github.com/donavanbecker). All rights reserved.
2
+ *
3
+ * getstate-cache.spec.ts: one fetch serves a burst of reads
4
+ */
5
+
6
+ import { describe, expect, it, vi } from 'vitest'
7
+
8
+ import { GenericDevice } from '../../src/devices/genericDevice.js'
9
+
10
+ const log: any = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), success: vi.fn() }
11
+
12
+ function device() {
13
+ const getDevice = vi.fn(async () => ({ getStatus: async () => ({ temperature: 22.7, humidity: 59 }) }))
14
+ const d = new GenericDevice({ id: 'B0E9FED044E3', type: 'meter', name: 'M', log }, { log, _client: { getDevice } } as any)
15
+ return { d, getDevice }
16
+ }
17
+
18
+ describe('genericDevice.getState caching', () => {
19
+ // HomeKit reads every characteristic of an accessory at once. Seven
20
+ // characteristics must not mean seven API requests.
21
+ it('fetches once for a burst of concurrent reads', async () => {
22
+ const { d, getDevice } = device()
23
+
24
+ const states = await Promise.all([d.getState(), d.getState(), d.getState(), d.getState()])
25
+
26
+ expect(getDevice).toHaveBeenCalledTimes(1)
27
+ expect(states.every(s => s.temperature === 22.7)).toBe(true)
28
+ })
29
+
30
+ it('reuses the value for sequential reads inside the window', async () => {
31
+ const { d, getDevice } = device()
32
+
33
+ await d.getState()
34
+ await d.getState()
35
+
36
+ expect(getDevice).toHaveBeenCalledTimes(1)
37
+ })
38
+
39
+ it('fetches again once the window has passed', async () => {
40
+ const { d, getDevice } = device()
41
+ await d.getState()
42
+
43
+ vi.spyOn(Date, 'now').mockReturnValue(Date.now() + 20_000)
44
+ await d.getState()
45
+ vi.restoreAllMocks()
46
+
47
+ expect(getDevice).toHaveBeenCalledTimes(2)
48
+ })
49
+
50
+ it('marks an unreadable device and does not cache it', async () => {
51
+ const getDevice = vi.fn()
52
+ .mockRejectedValueOnce(new Error('offline'))
53
+ .mockResolvedValueOnce({ getStatus: async () => ({ temperature: 21 }) })
54
+ const d = new GenericDevice({ id: 'x', type: 'meter', name: 'M', log }, { log, _client: { getDevice } } as any)
55
+
56
+ const first = await d.getState()
57
+ const second = await d.getState()
58
+
59
+ expect(first).toEqual({ id: 'x', type: 'meter', unreadable: true })
60
+ expect(second.temperature).toBe(21)
61
+ })
62
+ })
@@ -0,0 +1,89 @@
1
+ /* Copyright(C) 2021-2026, donavanbecker (https://github.com/donavanbecker). All rights reserved.
2
+ *
3
+ * getstate-returns-status.spec.ts: getState() must yield readings, not the device
4
+ */
5
+
6
+ import { describe, expect, it, vi } from 'vitest'
7
+
8
+ import { createDevice } from '../../src/deviceFactory.js'
9
+ import { GenericDevice, MeterDevice } from '../../src/devices/genericDevice.js'
10
+
11
+ const log: any = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), success: vi.fn() }
12
+
13
+ /** A node-switchbot style device: identity on the instance, readings via getStatus(). */
14
+ function switchBotDevice(status: any) {
15
+ return {
16
+ id: 'B0E9FED044E3',
17
+ name: 'Meter Pro CO2 Monitor',
18
+ deviceType: 'MeterPro(CO2)',
19
+ mac: 'b0:e9:fe:d0:44:e3',
20
+ getStatus: async () => status,
21
+ }
22
+ }
23
+
24
+ const STATUS = { deviceId: 'B0E9FED044E3', temperature: 23.7, humidity: 59, battery: 100, connectionType: 'api' }
25
+
26
+ function deviceWith(client: any) {
27
+ return new GenericDevice({ id: 'B0E9FED044E3', type: 'meter', name: 'Meter', log }, { log, _client: client } as any)
28
+ }
29
+
30
+ describe('genericDevice.getState', () => {
31
+ it('returns the status rather than the device instance', async () => {
32
+ const state = await deviceWith({ getDevice: async () => switchBotDevice(STATUS) }).getState()
33
+
34
+ expect(state).toEqual(STATUS)
35
+ expect(state.temperature).toBe(23.7)
36
+ expect(state.humidity).toBe(59)
37
+ })
38
+
39
+ it('passes through a plain status object unchanged', async () => {
40
+ const state = await deviceWith({ getDevice: async () => ({ temperature: 20, humidity: 40 }) }).getState()
41
+
42
+ expect(state).toEqual({ temperature: 20, humidity: 40 })
43
+ })
44
+
45
+ it('unwraps an API-shaped body', async () => {
46
+ const state = await deviceWith({ getDevice: async () => ({ body: { temperature: 19 } }) }).getState()
47
+
48
+ expect(state).toEqual({ temperature: 19 })
49
+ })
50
+
51
+ it.each([
52
+ ['throws', async () => { throw new Error('unreachable') }],
53
+ ['returns null', async () => null],
54
+ ])('reports minimal info when getStatus() %s', async (_label, getStatus) => {
55
+ const client = { getDevice: async () => ({ ...switchBotDevice(STATUS), getStatus }) }
56
+
57
+ const state = await deviceWith(client).getState()
58
+
59
+ // Not the device instance: that would look like a reading of every field.
60
+ expect(state).toEqual({ id: 'B0E9FED044E3', type: 'meter', unreadable: true })
61
+ expect(state.temperature).toBeUndefined()
62
+ })
63
+ })
64
+
65
+ describe('createDevice', () => {
66
+ it('does not replace the device getState with the raw client device', async () => {
67
+ const client = { init: async () => {}, getDevice: async () => switchBotDevice(STATUS) }
68
+ const created: any = await createDevice(
69
+ { id: 'B0E9FED044E3', type: 'meter', name: 'Meter', log } as any,
70
+ { log, _client: client } as any,
71
+ false,
72
+ )
73
+
74
+ const state = await created.instance.getState()
75
+
76
+ expect(state.temperature).toBe(23.7)
77
+ expect(state.getStatus).toBeUndefined()
78
+ })
79
+
80
+ it('lets a meter report a real temperature end to end', async () => {
81
+ const client = { init: async () => {}, getDevice: async () => switchBotDevice(STATUS) }
82
+ const device = new MeterDevice({ id: 'B0E9FED044E3', type: 'meter', name: 'Meter', log } as any, { log, _client: client } as any)
83
+
84
+ const services = device.createHAPAccessory(null).services
85
+ const temperature = services.find((s: any) => s.type === 'TemperatureSensor')
86
+
87
+ await expect(temperature.characteristics.CurrentTemperature.get()).resolves.toBe(23.7)
88
+ })
89
+ })