@switchbot/homebridge-switchbot 5.0.0 → 5.1.0-beta.1
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/npm-version-script-esm.js +97 -0
- package/CHANGELOG.md +21 -8
- package/MIGRATION.md +5 -0
- package/README.md +25 -0
- package/config.schema.json +11 -0
- package/dist/SwitchBotHAPPlatform.d.ts.map +1 -1
- package/dist/SwitchBotHAPPlatform.js +33 -4
- package/dist/SwitchBotHAPPlatform.js.map +1 -1
- package/dist/SwitchBotMatterPlatform.d.ts +2 -0
- package/dist/SwitchBotMatterPlatform.d.ts.map +1 -1
- package/dist/SwitchBotMatterPlatform.js +61 -5
- package/dist/SwitchBotMatterPlatform.js.map +1 -1
- package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/config.js +9 -2
- package/dist/homebridge-ui/endpoints/config.js.map +1 -1
- package/dist/homebridge-ui/homebridge-ui/endpoints/config.js +9 -2
- package/dist/homebridge-ui/homebridge-ui/endpoints/config.js.map +1 -1
- package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js +8 -0
- package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js.map +1 -1
- package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js +23 -0
- package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js.map +1 -0
- package/dist/homebridge-ui/public/css/styles.css +61 -1
- package/dist/homebridge-ui/public/index.html +25 -2
- package/dist/homebridge-ui/public/js/api.d.ts +16 -0
- package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/api.js +48 -0
- package/dist/homebridge-ui/public/js/api.js.map +1 -1
- package/dist/homebridge-ui/public/js/api.ts +61 -1
- package/dist/homebridge-ui/public/js/app.js +237 -111
- package/dist/homebridge-ui/public/js/app.js.map +4 -4
- package/dist/homebridge-ui/public/js/app.ts +22 -4
- package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/devices.js +6 -0
- package/dist/homebridge-ui/public/js/devices.js.map +1 -1
- package/dist/homebridge-ui/public/js/devices.ts +6 -0
- package/dist/homebridge-ui/public/js/modals.d.ts +1 -0
- package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/modals.js +53 -0
- package/dist/homebridge-ui/public/js/modals.js.map +1 -1
- package/dist/homebridge-ui/public/js/modals.ts +58 -0
- package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/render.js +2 -1
- package/dist/homebridge-ui/public/js/render.js.map +1 -1
- package/dist/homebridge-ui/public/js/render.ts +2 -1
- package/dist/homebridge-ui/settings.js.map +1 -1
- package/dist/homebridge-ui/utils/config-parser.d.ts +8 -0
- package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -1
- package/dist/homebridge-ui/utils/config-parser.js +8 -0
- package/dist/homebridge-ui/utils/config-parser.js.map +1 -1
- package/dist/homebridge-ui/utils/v4-detection.d.ts +12 -0
- package/dist/homebridge-ui/utils/v4-detection.d.ts.map +1 -0
- package/dist/homebridge-ui/utils/v4-detection.js +23 -0
- package/dist/homebridge-ui/utils/v4-detection.js.map +1 -0
- package/dist/settings.d.ts +11 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js.map +1 -1
- package/dist/utils.d.ts +5 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +27 -0
- package/dist/utils.js.map +1 -1
- package/docs/assets/highlight.css +15 -15
- package/docs/assets/icons.js +1 -1
- package/docs/assets/icons.svg +1 -1
- package/docs/assets/main.js +3 -3
- package/docs/index.html +22 -5
- package/docs/variables/default.html +1 -1
- package/package.json +6 -6
- package/scripts/build-ui.js +30 -0
- package/src/SwitchBotHAPPlatform.ts +31 -6
- package/src/SwitchBotMatterPlatform.ts +61 -7
- package/src/homebridge-ui/endpoints/config.ts +10 -2
- package/src/homebridge-ui/public/css/styles.css +61 -1
- package/src/homebridge-ui/public/index.html +23 -0
- package/src/homebridge-ui/public/js/api.ts +61 -1
- package/src/homebridge-ui/public/js/app.ts +22 -4
- package/src/homebridge-ui/public/js/devices.ts +6 -0
- package/src/homebridge-ui/public/js/modals.ts +58 -0
- package/src/homebridge-ui/public/js/render.ts +2 -1
- package/src/homebridge-ui/utils/config-parser.ts +9 -0
- package/src/homebridge-ui/utils/v4-detection.ts +23 -0
- package/src/settings.ts +12 -0
- package/src/utils.ts +31 -0
- package/test/homebridge-ui/config-parser.spec.ts +61 -0
- package/test/platform/matter-integration.spec.ts +50 -0
- package/test/utils/utils.spec.ts +15 -0
|
@@ -11,6 +11,29 @@
|
|
|
11
11
|
<body>
|
|
12
12
|
<h1>🤖 SwitchBot Configuration</h1>
|
|
13
13
|
|
|
14
|
+
<!-- v4 config incompatibility warning – shown dynamically by app.js -->
|
|
15
|
+
<div id="v4ConfigWarning" class="v4-config-warning" style="display: none">
|
|
16
|
+
<strong>⚠️ Incompatible v4 Configuration Detected</strong>
|
|
17
|
+
<p>
|
|
18
|
+
Your config.json appears to be in the <strong>v4 format</strong> (using <code>credentials</code> and
|
|
19
|
+
<code>options.devices</code> fields). This format is <strong>not compatible with v5 or newer</strong> of the
|
|
20
|
+
SwitchBot plugin. Devices will not be recognised until the config is updated.
|
|
21
|
+
</p>
|
|
22
|
+
<p>
|
|
23
|
+
Please update your config to the v5 format:
|
|
24
|
+
</p>
|
|
25
|
+
<ul>
|
|
26
|
+
<li>Move <code>credentials.token</code> → <code>openApiToken</code> (top level)</li>
|
|
27
|
+
<li>Move <code>credentials.secret</code> → <code>openApiSecret</code> (top level)</li>
|
|
28
|
+
<li>Move <code>options.devices</code> → <code>devices</code> (top level)</li>
|
|
29
|
+
<li>Rename per-device <code>connectionType</code> → <code>connectionPreference</code></li>
|
|
30
|
+
<li>Remove the <code>credentials</code> and <code>options</code> wrapper objects</li>
|
|
31
|
+
</ul>
|
|
32
|
+
<p>
|
|
33
|
+
See the <a href="https://github.com/OpenWonderLabs/homebridge-switchbot/blob/latest/MIGRATION.md" target="_blank" rel="noopener">Migration Guide</a> for full details.
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
14
37
|
<div class="card compact">
|
|
15
38
|
<h2>API Credentials</h2>
|
|
16
39
|
<div class="credentials-row">
|
|
@@ -1,5 +1,6 @@
|
|
|
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'
|
|
3
4
|
import './types.js'
|
|
4
5
|
import { uiLog } from './logger.js'
|
|
5
6
|
import { toastError } from './toast.js'
|
|
@@ -22,6 +23,35 @@ export async function fetchDevices(): Promise<any[]> {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
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
|
+
|
|
25
55
|
/**
|
|
26
56
|
* Validate and auto-correct device types in the config array before saving.
|
|
27
57
|
* Returns an array of errors for devices that cannot be fixed.
|
|
@@ -189,7 +219,19 @@ export async function addDevice(
|
|
|
189
219
|
deviceId: string,
|
|
190
220
|
name: string,
|
|
191
221
|
type: string,
|
|
192
|
-
options?: {
|
|
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
|
+
},
|
|
193
235
|
): Promise<any> {
|
|
194
236
|
if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') {
|
|
195
237
|
throw new TypeError('Homebridge UI API not available')
|
|
@@ -224,6 +266,24 @@ export async function addDevice(
|
|
|
224
266
|
if (options?.keyId) {
|
|
225
267
|
newDevice.keyId = options.keyId
|
|
226
268
|
}
|
|
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
|
+
}
|
|
227
287
|
config.devices.push(newDevice)
|
|
228
288
|
await homebridge.updatePluginConfig(configArr)
|
|
229
289
|
if (typeof homebridge.savePluginConfig === 'function') {
|
|
@@ -1,13 +1,31 @@
|
|
|
1
|
+
import { detectV4Config } from './api.js'
|
|
1
2
|
import { loadCredentialStatus, saveCredentials } from './credentials.js'
|
|
2
3
|
import { initRemoveAllButton, loadConfiguredDevices } from './devices.js'
|
|
3
|
-
import { discoverDevices, initializeDiscoverySettings } from './discovery.js'
|
|
4
|
+
import { discoverDevices, initializeDiscoverySettings } from './discovery.js'
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
(window as any).
|
|
6
|
+
;
|
|
7
|
+
|
|
8
|
+
(window as any).loadCredentialStatus = loadCredentialStatus
|
|
9
|
+
;(window as any).saveCredentials = saveCredentials
|
|
10
|
+
;(window as any).discoverDevices = discoverDevices
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Show or hide the v4 config incompatibility warning banner.
|
|
14
|
+
*/
|
|
15
|
+
async function checkV4Config(): Promise<void> {
|
|
16
|
+
const banner = document.getElementById('v4ConfigWarning')
|
|
17
|
+
if (!banner) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
const isV4 = await detectV4Config()
|
|
21
|
+
if (isV4) {
|
|
22
|
+
banner.style.display = 'block'
|
|
23
|
+
}
|
|
24
|
+
}
|
|
8
25
|
|
|
9
26
|
// Initialize on page load
|
|
10
27
|
async function init(): Promise<void> {
|
|
28
|
+
await checkV4Config()
|
|
11
29
|
await loadCredentialStatus()
|
|
12
30
|
await initializeDiscoverySettings()
|
|
13
31
|
await loadConfiguredDevices()
|
|
@@ -26,8 +26,14 @@ 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,
|
|
29
32
|
encryptionKey: importValues.encryptionKey,
|
|
30
33
|
keyId: importValues.keyId,
|
|
34
|
+
refreshRate: importValues.refreshRate,
|
|
35
|
+
blePollingEnabled: importValues.blePollingEnabled,
|
|
36
|
+
blePollIntervalMs: importValues.blePollIntervalMs,
|
|
31
37
|
})
|
|
32
38
|
uiLog.info('Add device response:', resp)
|
|
33
39
|
|
|
@@ -6,6 +6,7 @@ export type ImportDiscoveredDeviceResult = {
|
|
|
6
6
|
configDeviceType: string
|
|
7
7
|
address?: string
|
|
8
8
|
connectionPreference?: string
|
|
9
|
+
externalPublishProtocol?: string
|
|
9
10
|
room?: string
|
|
10
11
|
encryptionKey?: string
|
|
11
12
|
keyId?: string
|
|
@@ -154,6 +155,30 @@ export async function importDiscoveredDevice(device: any): Promise<ImportDiscove
|
|
|
154
155
|
connectionPrefSelect.appendChild(opt)
|
|
155
156
|
})
|
|
156
157
|
|
|
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
|
+
|
|
157
182
|
// --- Room ---
|
|
158
183
|
const roomLabel = document.createElement('label')
|
|
159
184
|
roomLabel.textContent = 'Room'
|
|
@@ -308,6 +333,7 @@ export async function importDiscoveredDevice(device: any): Promise<ImportDiscove
|
|
|
308
333
|
configDeviceType: typeSelect.value || device.type,
|
|
309
334
|
address: macInput.value || undefined,
|
|
310
335
|
connectionPreference: connectionPrefSelect.value || undefined,
|
|
336
|
+
externalPublishProtocol: externalPublishSelect.value || 'none',
|
|
311
337
|
room: roomInput.value || undefined,
|
|
312
338
|
encryptionKey: encryptionKeyInput.value || undefined,
|
|
313
339
|
keyId: keyIdInput.value || undefined,
|
|
@@ -331,6 +357,8 @@ export async function importDiscoveredDevice(device: any): Promise<ImportDiscove
|
|
|
331
357
|
contentDiv.appendChild(typeSelect)
|
|
332
358
|
contentDiv.appendChild(connectionPrefLabel)
|
|
333
359
|
contentDiv.appendChild(connectionPrefSelect)
|
|
360
|
+
contentDiv.appendChild(externalPublishLabel)
|
|
361
|
+
contentDiv.appendChild(externalPublishSelect)
|
|
334
362
|
contentDiv.appendChild(roomLabel)
|
|
335
363
|
contentDiv.appendChild(roomInput)
|
|
336
364
|
contentDiv.appendChild(macLabel)
|
|
@@ -573,6 +601,30 @@ export async function editDevice(device: any): Promise<void> {
|
|
|
573
601
|
connectionPrefSelect.appendChild(opt)
|
|
574
602
|
})
|
|
575
603
|
|
|
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
|
+
|
|
576
628
|
// --- Room ---
|
|
577
629
|
const roomLabel = document.createElement('label')
|
|
578
630
|
roomLabel.textContent = 'Room'
|
|
@@ -674,6 +726,7 @@ export async function editDevice(device: any): Promise<void> {
|
|
|
674
726
|
configDeviceName: nameInput.value || undefined,
|
|
675
727
|
configDeviceType: typeSelect.value,
|
|
676
728
|
connectionPreference: connectionPrefSelect.value,
|
|
729
|
+
externalPublishProtocol: externalPublishSelect.value,
|
|
677
730
|
room: roomInput.value || undefined,
|
|
678
731
|
encryptionKey: encryptionKeyInput.value || undefined,
|
|
679
732
|
keyId: keyIdInput.value || undefined,
|
|
@@ -690,6 +743,9 @@ export async function editDevice(device: any): Promise<void> {
|
|
|
690
743
|
if (params.room !== undefined) {
|
|
691
744
|
options.room = params.room
|
|
692
745
|
}
|
|
746
|
+
if (params.externalPublishProtocol !== undefined) {
|
|
747
|
+
options.externalPublishProtocol = params.externalPublishProtocol
|
|
748
|
+
}
|
|
693
749
|
if (params.encryptionKey !== undefined) {
|
|
694
750
|
options.encryptionKey = params.encryptionKey
|
|
695
751
|
}
|
|
@@ -739,6 +795,8 @@ export async function editDevice(device: any): Promise<void> {
|
|
|
739
795
|
contentDiv.appendChild(typeSelect)
|
|
740
796
|
contentDiv.appendChild(connectionPrefLabel)
|
|
741
797
|
contentDiv.appendChild(connectionPrefSelect)
|
|
798
|
+
contentDiv.appendChild(externalPublishLabel)
|
|
799
|
+
contentDiv.appendChild(externalPublishSelect)
|
|
742
800
|
contentDiv.appendChild(roomLabel)
|
|
743
801
|
contentDiv.appendChild(roomInput)
|
|
744
802
|
contentDiv.appendChild(encryptionKeyLabel)
|
|
@@ -1011,8 +1011,9 @@ export function renderDeviceList(list: any[]): void {
|
|
|
1011
1011
|
|
|
1012
1012
|
const typeText = d.type ? `type: ${d.type}` : ''
|
|
1013
1013
|
const connText = d.connectionPreference ? `conn: ${d.connectionPreference}` : ''
|
|
1014
|
+
const extText = d.externalPublishProtocol && d.externalPublishProtocol !== 'none' ? `external: ${d.externalPublishProtocol}` : ''
|
|
1014
1015
|
const roomText = d.room ? `room: ${d.room}` : ''
|
|
1015
|
-
meta.textContent = [typeText, connText, roomText].filter(Boolean).join(' | ')
|
|
1016
|
+
meta.textContent = [typeText, connText, extText, roomText].filter(Boolean).join(' | ')
|
|
1016
1017
|
|
|
1017
1018
|
info.appendChild(nameContainer)
|
|
1018
1019
|
info.appendChild(meta)
|
|
@@ -57,6 +57,15 @@ 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
|
+
|
|
60
69
|
/**
|
|
61
70
|
* Get credential from platform config
|
|
62
71
|
*/
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect whether a platform config block is in the legacy v4 format.
|
|
3
|
+
*
|
|
4
|
+
* v4 configs stored credentials under a `credentials` sub-object and devices
|
|
5
|
+
* under `options.devices`. v5+ flattened these to `openApiToken`,
|
|
6
|
+
* `openApiSecret`, and a root-level `devices` array.
|
|
7
|
+
*
|
|
8
|
+
* This helper has no Node.js-specific dependencies so it can be imported from
|
|
9
|
+
* both the Homebridge server-side code and the browser-side plugin UI bundle.
|
|
10
|
+
*/
|
|
11
|
+
export function isV4Config(platform: any): boolean {
|
|
12
|
+
if (!platform || typeof platform !== 'object') {
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
// Presence of a `credentials` sub-object is the clearest v4 signal
|
|
16
|
+
const hasCredentials = platform.credentials !== undefined && typeof platform.credentials === 'object'
|
|
17
|
+
// Presence of `options.devices` is the other v4 signal
|
|
18
|
+
const hasOptionsDevices
|
|
19
|
+
= platform.options !== undefined
|
|
20
|
+
&& typeof platform.options === 'object'
|
|
21
|
+
&& Array.isArray(platform.options.devices)
|
|
22
|
+
return hasCredentials || hasOptionsDevices
|
|
23
|
+
}
|
package/src/settings.ts
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
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
|
+
|
|
4
15
|
export interface SwitchBotPluginConfig {
|
|
5
16
|
openApiToken?: string
|
|
6
17
|
openApiSecret?: string
|
|
7
18
|
preferMatter?: boolean
|
|
8
19
|
enableMatter?: boolean
|
|
9
20
|
enableBLE?: boolean // Enable or disable BLE support
|
|
21
|
+
devices?: SwitchBotDeviceConfig[]
|
|
10
22
|
// other plugin-specific configuration
|
|
11
23
|
[key: string]: any
|
|
12
24
|
}
|
package/src/utils.ts
CHANGED
|
@@ -1137,6 +1137,37 @@ 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
|
+
|
|
1140
1171
|
// Create a Proxy constructor that instantiates the right platform implementation at runtime.
|
|
1141
1172
|
/**
|
|
1142
1173
|
* Creates a proxy class that instantiates the correct platform implementation (HAP or Matter) at runtime.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { isV4Config } from '../../src/homebridge-ui/utils/v4-detection.js'
|
|
4
|
+
|
|
5
|
+
describe('isV4Config', () => {
|
|
6
|
+
it('returns false for null/undefined input', () => {
|
|
7
|
+
expect(isV4Config(null)).toBe(false)
|
|
8
|
+
expect(isV4Config(undefined)).toBe(false)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('returns false for an empty v5 config block', () => {
|
|
12
|
+
expect(isV4Config({ platform: 'SwitchBot', devices: [] })).toBe(false)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('returns false for a v5 config with credentials omitted', () => {
|
|
16
|
+
expect(isV4Config({ platform: 'SwitchBot', openApiToken: 'tok', openApiSecret: 'sec', devices: [] })).toBe(false)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('returns true when a credentials sub-object is present (v4 token location)', () => {
|
|
20
|
+
const v4 = {
|
|
21
|
+
platform: 'SwitchBot',
|
|
22
|
+
credentials: { token: 'tok', secret: 'sec', notice: 'Keep your token a secret!' },
|
|
23
|
+
options: { devices: [] },
|
|
24
|
+
}
|
|
25
|
+
expect(isV4Config(v4)).toBe(true)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('returns true when options.devices array is present without credentials (partial v4)', () => {
|
|
29
|
+
const partialV4 = {
|
|
30
|
+
platform: 'SwitchBot',
|
|
31
|
+
options: {
|
|
32
|
+
devices: [{ deviceId: 'aabbcc', configDeviceName: 'Curtain', configDeviceType: 'Curtain', connectionType: 'BLE' }],
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
expect(isV4Config(partialV4)).toBe(true)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('returns true for the exact config from the bug report', () => {
|
|
39
|
+
const bugReportConfig = {
|
|
40
|
+
name: 'SwitchBot',
|
|
41
|
+
credentials: { notice: 'Keep your token a secret!' },
|
|
42
|
+
options: {
|
|
43
|
+
devices: [
|
|
44
|
+
{
|
|
45
|
+
deviceId: 'xxxxxxxx',
|
|
46
|
+
configDeviceName: 'Curtain',
|
|
47
|
+
configDeviceType: 'Curtain',
|
|
48
|
+
connectionType: 'BLE',
|
|
49
|
+
hide_lightsensor: false,
|
|
50
|
+
set_min: 96,
|
|
51
|
+
set_max: 12,
|
|
52
|
+
maxRetry: 0,
|
|
53
|
+
logging: 'standard',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
platform: 'SwitchBot',
|
|
58
|
+
}
|
|
59
|
+
expect(isV4Config(bugReportConfig)).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -30,4 +30,54 @@ describe('matter integration platform', () => {
|
|
|
30
30
|
const platform = new TestMatterPlatform(logger, config, api)
|
|
31
31
|
expect(platform.getDevices()).toEqual([])
|
|
32
32
|
})
|
|
33
|
+
|
|
34
|
+
it('should publish per-device external Matter accessories via the Homebridge external event', async () => {
|
|
35
|
+
const pendingExternalRegistrations = new Map()
|
|
36
|
+
const apiWithMatter: any = {
|
|
37
|
+
matter: {
|
|
38
|
+
uuid: { generate: (value: string) => `m-${value}` },
|
|
39
|
+
registerPlatformAccessories: vi.fn(async () => {}),
|
|
40
|
+
},
|
|
41
|
+
hap: { uuid: { generate: (value: string) => `h-${value}` } },
|
|
42
|
+
isMatterAvailable: () => true,
|
|
43
|
+
isMatterEnabled: () => true,
|
|
44
|
+
on: vi.fn(),
|
|
45
|
+
emit: null as any,
|
|
46
|
+
_pendingExternalRegistrations: pendingExternalRegistrations,
|
|
47
|
+
}
|
|
48
|
+
const emit = vi.fn((event: string, accessories: any[], registrationId: string) => {
|
|
49
|
+
if (event === 'publishExternalMatterAccessories') {
|
|
50
|
+
const resolve = pendingExternalRegistrations.get(registrationId)
|
|
51
|
+
resolve?.()
|
|
52
|
+
}
|
|
53
|
+
return true
|
|
54
|
+
})
|
|
55
|
+
apiWithMatter.emit = emit
|
|
56
|
+
const platform = new TestMatterPlatform(logger, config, apiWithMatter)
|
|
57
|
+
const created = {
|
|
58
|
+
createAccessory: vi.fn(async () => ({
|
|
59
|
+
name: 'Curtain3',
|
|
60
|
+
manufacturer: 'SwitchBot',
|
|
61
|
+
model: 'Curtain3',
|
|
62
|
+
serialNumber: 'serial-1',
|
|
63
|
+
deviceType: { deviceType: 514 },
|
|
64
|
+
clusters: { windowCovering: { currentPositionLiftPercent100ths: 0 } },
|
|
65
|
+
handlers: {},
|
|
66
|
+
})),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await platform.registerMatterAccessories([
|
|
70
|
+
{
|
|
71
|
+
created,
|
|
72
|
+
d: { id: 'device-1', name: 'Curtain3' },
|
|
73
|
+
type: 'curtain',
|
|
74
|
+
useMatter: true,
|
|
75
|
+
matterAvailable: true,
|
|
76
|
+
externalPublishProtocol: 'matter',
|
|
77
|
+
},
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
expect(emit).toHaveBeenCalledWith('publishExternalMatterAccessories', expect.any(Array), expect.any(String))
|
|
81
|
+
expect(apiWithMatter.matter.registerPlatformAccessories).not.toHaveBeenCalled()
|
|
82
|
+
})
|
|
33
83
|
})
|
package/test/utils/utils.spec.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
MATTER_ATTRIBUTE_IDS,
|
|
6
6
|
MATTER_CLUSTER_IDS,
|
|
7
7
|
normalizeConfig,
|
|
8
|
+
resolveExternalPublishProtocol,
|
|
8
9
|
} from '../../src/utils'
|
|
9
10
|
|
|
10
11
|
describe('utils', () => {
|
|
@@ -50,4 +51,18 @@ describe('utils', () => {
|
|
|
50
51
|
expect(HAP).toHaveBeenCalled()
|
|
51
52
|
expect(Matter).not.toHaveBeenCalled()
|
|
52
53
|
})
|
|
54
|
+
|
|
55
|
+
it('resolveExternalPublishProtocol normalizes known values and aliases', () => {
|
|
56
|
+
expect(resolveExternalPublishProtocol({ externalPublishProtocol: 'hap' })).toBe('hap')
|
|
57
|
+
expect(resolveExternalPublishProtocol({ externalPublishProtocol: 'matter' })).toBe('matter')
|
|
58
|
+
expect(resolveExternalPublishProtocol({ externalPublishProtocol: 'none' })).toBe('none')
|
|
59
|
+
expect(resolveExternalPublishProtocol({ externalPublishProtocol: 'homekit' })).toBe('hap')
|
|
60
|
+
expect(resolveExternalPublishProtocol({ publishExternal: true })).toBe('hap')
|
|
61
|
+
expect(resolveExternalPublishProtocol({ publishExternal: false })).toBe('none')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('resolveExternalPublishProtocol defaults to none for unknown values', () => {
|
|
65
|
+
expect(resolveExternalPublishProtocol({ externalPublishProtocol: 'invalid' })).toBe('none')
|
|
66
|
+
expect(resolveExternalPublishProtocol({})).toBe('none')
|
|
67
|
+
})
|
|
53
68
|
})
|