@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.
- package/.github/copilot-instructions.md +0 -63
- package/.github/scripts/normalize-changelog-header.cjs +1 -1
- package/.github/scripts/npm-version-script-esm-auto.js +1 -1
- package/.github/scripts/resolve-version-bump-esm.js +1 -1
- package/CHANGELOG.md +10 -9
- package/MIGRATION.md +0 -5
- package/README.md +0 -25
- package/config.schema.json +0 -11
- package/dist/{Platform.HAP.d.ts → SwitchBotHAPPlatform.d.ts} +16 -2
- package/dist/SwitchBotHAPPlatform.d.ts.map +1 -0
- package/dist/{Platform.HAP.js → SwitchBotHAPPlatform.js} +111 -40
- package/dist/SwitchBotHAPPlatform.js.map +1 -0
- package/dist/{Platform.Matter.d.ts → SwitchBotMatterPlatform.d.ts} +2 -4
- package/dist/SwitchBotMatterPlatform.d.ts.map +1 -0
- package/dist/{Platform.Matter.js → SwitchBotMatterPlatform.js} +6 -62
- package/dist/SwitchBotMatterPlatform.js.map +1 -0
- package/dist/deviceFactory.d.ts.map +1 -1
- package/dist/deviceFactory.js +5 -15
- package/dist/deviceFactory.js.map +1 -1
- package/dist/devices/genericDevice.d.ts +63 -5
- package/dist/devices/genericDevice.d.ts.map +1 -1
- package/dist/devices/genericDevice.js +251 -11
- package/dist/devices/genericDevice.js.map +1 -1
- package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/config.js +2 -9
- package/dist/homebridge-ui/endpoints/config.js.map +1 -1
- package/dist/homebridge-ui/homebridge-ui/endpoints/config.js +2 -9
- package/dist/homebridge-ui/homebridge-ui/endpoints/config.js.map +1 -1
- package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js +0 -8
- package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js.map +1 -1
- package/dist/homebridge-ui/public/css/styles.css +1 -61
- package/dist/homebridge-ui/public/index.html +2 -61
- package/dist/homebridge-ui/public/js/advanced-settings.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/advanced-settings.js +3 -4
- package/dist/homebridge-ui/public/js/advanced-settings.js.map +1 -1
- package/dist/homebridge-ui/public/js/advanced-settings.ts +3 -4
- package/dist/homebridge-ui/public/js/api.d.ts +0 -16
- package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/api.js +0 -48
- package/dist/homebridge-ui/public/js/api.js.map +1 -1
- package/dist/homebridge-ui/public/js/api.ts +1 -61
- package/dist/homebridge-ui/public/js/app.js +115 -267
- package/dist/homebridge-ui/public/js/app.js.map +4 -4
- package/dist/homebridge-ui/public/js/app.ts +4 -47
- package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/credentials.js +5 -11
- package/dist/homebridge-ui/public/js/credentials.js.map +1 -1
- package/dist/homebridge-ui/public/js/credentials.ts +5 -11
- package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/devices.js +0 -6
- package/dist/homebridge-ui/public/js/devices.js.map +1 -1
- package/dist/homebridge-ui/public/js/devices.ts +0 -6
- package/dist/homebridge-ui/public/js/modals.d.ts +0 -1
- package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/modals.js +0 -53
- package/dist/homebridge-ui/public/js/modals.js.map +1 -1
- package/dist/homebridge-ui/public/js/modals.ts +0 -58
- package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/render.js +1 -2
- package/dist/homebridge-ui/public/js/render.js.map +1 -1
- package/dist/homebridge-ui/public/js/render.ts +1 -2
- package/dist/homebridge-ui/settings.js.map +1 -1
- package/dist/homebridge-ui/utils/config-parser.d.ts +0 -8
- package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -1
- package/dist/homebridge-ui/utils/config-parser.js +0 -8
- package/dist/homebridge-ui/utils/config-parser.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/settings.d.ts +0 -11
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js.map +1 -1
- package/dist/utils.d.ts +0 -5
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +0 -27
- package/dist/utils.js.map +1 -1
- package/docs/assets/highlight.css +15 -15
- package/docs/index.html +5 -22
- package/docs/variables/default.html +1 -1
- package/package.json +15 -9
- package/scripts/build-ui.js +0 -30
- package/src/{Platform.HAP.ts → SwitchBotHAPPlatform.ts} +112 -39
- package/src/{Platform.Matter.ts → SwitchBotMatterPlatform.ts} +8 -63
- package/src/deviceFactory.ts +5 -14
- package/src/devices/genericDevice.ts +271 -12
- package/src/homebridge-ui/endpoints/config.ts +2 -10
- package/src/homebridge-ui/public/css/styles.css +1 -61
- package/src/homebridge-ui/public/index.html +0 -59
- package/src/homebridge-ui/public/js/advanced-settings.ts +3 -4
- package/src/homebridge-ui/public/js/api.ts +1 -61
- package/src/homebridge-ui/public/js/app.ts +4 -47
- package/src/homebridge-ui/public/js/credentials.ts +5 -11
- package/src/homebridge-ui/public/js/devices.ts +0 -6
- package/src/homebridge-ui/public/js/modals.ts +0 -58
- package/src/homebridge-ui/public/js/render.ts +1 -2
- package/src/homebridge-ui/utils/config-parser.ts +0 -9
- package/src/index.ts +2 -2
- package/src/settings.ts +0 -12
- package/src/utils.ts +0 -31
- package/test/device/deviceFactory.spec.ts +9 -0
- package/test/device/getstate-cache.spec.ts +62 -0
- package/test/device/getstate-returns-status.spec.ts +89 -0
- package/test/device/water-detector-battery.spec.ts +81 -0
- package/test/device/water-detector-leak.spec.ts +43 -0
- package/test/hap-push-updates.spec.ts +150 -0
- package/test/platform/accessory-restore.spec.ts +2 -2
- package/test/platform/matter-childbridge.spec.ts +1 -1
- package/test/platform/matter-integration.spec.ts +1 -51
- package/test/platform/platform-edge.spec.ts +2 -2
- package/test/platform/platform.integration.spec.ts +1 -1
- package/test/utils/utils.spec.ts +0 -15
- package/.github/npm-version-script-esm.js +0 -97
- package/dist/Platform.HAP.d.ts.map +0 -1
- package/dist/Platform.HAP.js.map +0 -1
- package/dist/Platform.Matter.d.ts.map +0 -1
- package/dist/Platform.Matter.js.map +0 -1
- package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js +0 -23
- package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js.map +0 -1
- package/dist/homebridge-ui/public/js/legacy-view.d.ts +0 -3
- package/dist/homebridge-ui/public/js/legacy-view.d.ts.map +0 -1
- package/dist/homebridge-ui/public/js/legacy-view.js +0 -43
- package/dist/homebridge-ui/public/js/legacy-view.js.map +0 -1
- package/dist/homebridge-ui/public/js/legacy-view.ts +0 -99
- package/dist/homebridge-ui/utils/v4-detection.d.ts +0 -12
- package/dist/homebridge-ui/utils/v4-detection.d.ts.map +0 -1
- package/dist/homebridge-ui/utils/v4-detection.js +0 -23
- package/dist/homebridge-ui/utils/v4-detection.js.map +0 -1
- package/src/homebridge-ui/public/js/legacy-view.js +0 -43
- package/src/homebridge-ui/public/js/legacy-view.ts +0 -99
- package/src/homebridge-ui/utils/v4-detection.ts +0 -23
- 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
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
67
|
-
if (!Array.isArray(configArr)) {
|
|
68
|
-
|
|
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[
|
|
70
|
+
const config = configArr[0]
|
|
77
71
|
config.openApiToken = token
|
|
78
72
|
config.openApiSecret = secret
|
|
79
|
-
await homebridge.updatePluginConfig(
|
|
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,
|
|
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
|
+
})
|