@switchbot/homebridge-switchbot 5.1.0-beta.9 → 5.1.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/copilot-instructions.md +0 -63
- package/.github/scripts/compute-version-override.cjs +51 -0
- 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 +32 -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/device-types.d.ts.map +1 -1
- package/dist/device-types.js +10 -0
- package/dist/device-types.js.map +1 -1
- package/dist/deviceFactory.d.ts.map +1 -1
- package/dist/deviceFactory.js +10 -16
- 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/device-types.js +10 -0
- package/dist/homebridge-ui/device-types.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 +135 -272
- 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 +16 -16
- package/docs/assets/main.js +1 -1
- package/docs/assets/style.css +18 -3
- package/docs/hierarchy.html +26 -1
- package/docs/index.html +70 -23
- package/docs/modules.html +33 -1
- package/docs/variables/default.html +46 -3
- package/package.json +22 -15
- 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/device-types.ts +11 -0
- package/src/deviceFactory.ts +10 -15
- 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/model-identities.spec.ts +45 -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 -45
- package/dist/homebridge-ui/public/js/legacy-view.js.map +0 -1
- package/dist/homebridge-ui/public/js/legacy-view.ts +0 -107
- 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/device-types.js +0 -246
- package/src/homebridge-ui/public/js/legacy-view.js +0 -45
- package/src/homebridge-ui/public/js/legacy-view.ts +0 -107
- package/src/homebridge-ui/utils/v4-detection.ts +0 -23
- package/test/homebridge-ui/config-parser.spec.ts +0 -61
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { normalizeDeviceType } from '../../src/device-types'
|
|
4
|
+
import { createDevice } from '../../src/deviceFactory'
|
|
5
|
+
|
|
6
|
+
// node-switchbot 5 reports product-specific identities (pySwitchbot 3.0.0 parity).
|
|
7
|
+
// Every identity must reach the same accessory class as its canonical display name.
|
|
8
|
+
describe('product-specific model identities', () => {
|
|
9
|
+
const mockLogger = { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }
|
|
10
|
+
const dummyConfig = { logger: mockLogger, log: mockLogger }
|
|
11
|
+
|
|
12
|
+
it.each([
|
|
13
|
+
['WoPlugUS', 'Plug Mini (US)'],
|
|
14
|
+
['WoPlugJP', 'Plug Mini (JP)'],
|
|
15
|
+
['WoCurtain3', 'Curtain3'],
|
|
16
|
+
['WoMeterPlus', 'Meter Plus'],
|
|
17
|
+
['WoTHPc', 'Meter Pro (CO2)'],
|
|
18
|
+
['WoCeilingPro', 'Ceiling Light Pro'],
|
|
19
|
+
['WoLinkMini', 'Hub Mini'],
|
|
20
|
+
['WoIOSensorTH', 'WoIOSensor'],
|
|
21
|
+
['Outdoor Meter', 'WoIOSensor'],
|
|
22
|
+
['Indoor/Outdoor Thermo-Hygrometer', 'WoIOSensor'],
|
|
23
|
+
])('normalizes %s to %s', (input, expected) => {
|
|
24
|
+
expect(normalizeDeviceType(input)).toBe(expected)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it.each([
|
|
28
|
+
['WoPlugUS', 'PlugMiniDevice'],
|
|
29
|
+
['Plug Mini (US)', 'PlugMiniDevice'],
|
|
30
|
+
['WoPlugJP', 'PlugMiniDevice'],
|
|
31
|
+
['Plug Mini (JP)', 'PlugMiniDevice'],
|
|
32
|
+
['WoCurtain3', 'Curtain3Device'],
|
|
33
|
+
['Curtain3', 'Curtain3Device'],
|
|
34
|
+
['WoMeterPlus', 'MeterDevice'],
|
|
35
|
+
['Meter Plus', 'MeterDevice'],
|
|
36
|
+
['WoTHPc', 'MeterDevice'],
|
|
37
|
+
['Meter Pro (CO2)', 'MeterDevice'],
|
|
38
|
+
['Meter Pro', 'MeterDevice'],
|
|
39
|
+
['WoCeilingPro', 'LightDevice'],
|
|
40
|
+
['Ceiling Light Pro', 'LightDevice'],
|
|
41
|
+
])('creates the right accessory for %s', async (type, className) => {
|
|
42
|
+
const result = await createDevice({ id: `id-${type}`, type } as any, dummyConfig as any, false)
|
|
43
|
+
expect(result.instance.constructor.name).toBe(className)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { WaterDetectorDevice } from '../../src/devices/genericDevice'
|
|
4
|
+
|
|
5
|
+
const openApiMocks = vi.hoisted(() => {
|
|
6
|
+
const getStatus = vi.fn()
|
|
7
|
+
return {
|
|
8
|
+
getStatus,
|
|
9
|
+
// Use a function implementation so the mock is constructible with `new OpenAPIClient()`
|
|
10
|
+
// eslint-disable-next-line prefer-arrow-callback
|
|
11
|
+
OpenAPIClient: vi.fn().mockImplementation(function () { return { getStatus } }),
|
|
12
|
+
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
vi.mock('node-switchbot', () => ({
|
|
17
|
+
OpenAPIClient: openApiMocks.OpenAPIClient,
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
const mockLogger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }
|
|
21
|
+
|
|
22
|
+
const mockApi = {
|
|
23
|
+
hap: {
|
|
24
|
+
HapStatusError: class HapStatusError extends Error {
|
|
25
|
+
constructor(readonly status: number) {
|
|
26
|
+
super(`HAP ${status}`)
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
HAPStatus: {
|
|
30
|
+
SERVICE_COMMUNICATION_FAILURE: -70402,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeDevice(config: Record<string, unknown> = {}) {
|
|
36
|
+
return new WaterDetectorDevice(
|
|
37
|
+
{ id: 'wd1', type: 'waterdetector', name: 'Fridge', log: mockLogger, blePollingEnabled: false },
|
|
38
|
+
{ log: mockLogger, openApiToken: 'token', openApiSecret: 'secret', ...config } as any,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getBatteryService(device: WaterDetectorDevice) {
|
|
43
|
+
const accessory = device.createHAPAccessory(mockApi)
|
|
44
|
+
return accessory.services.find((service: any) => service.type === 'Battery')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('water detector device battery service', () => {
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
vi.clearAllMocks()
|
|
50
|
+
openApiMocks.getStatus.mockResolvedValue({ battery: 87 })
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('adds a Battery service to the HAP accessory descriptor', () => {
|
|
54
|
+
const batteryService = getBatteryService(makeDevice())
|
|
55
|
+
|
|
56
|
+
expect(batteryService).toBeDefined()
|
|
57
|
+
expect(batteryService.characteristics.BatteryLevel).toBeDefined()
|
|
58
|
+
expect(batteryService.characteristics.StatusLowBattery).toBeDefined()
|
|
59
|
+
expect(batteryService.characteristics.ChargingState).toBeDefined()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('warms the battery cache from direct OpenAPI status', async () => {
|
|
63
|
+
const device = makeDevice()
|
|
64
|
+
|
|
65
|
+
await device.init()
|
|
66
|
+
|
|
67
|
+
const batteryService = getBatteryService(device)
|
|
68
|
+
expect(openApiMocks.OpenAPIClient).toHaveBeenCalledWith('token', 'secret')
|
|
69
|
+
expect(openApiMocks.getStatus).toHaveBeenCalledWith('wd1')
|
|
70
|
+
expect(batteryService.characteristics.BatteryLevel.get()).toBe(87)
|
|
71
|
+
expect(batteryService.characteristics.StatusLowBattery.get()).toBe(0)
|
|
72
|
+
expect(batteryService.characteristics.ChargingState.get()).toBe(2)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('does not fabricate a battery level before one is cached', () => {
|
|
76
|
+
const device = makeDevice({ openApiSecret: undefined })
|
|
77
|
+
const batteryService = getBatteryService(device)
|
|
78
|
+
|
|
79
|
+
expect(() => batteryService.characteristics.BatteryLevel.get()).toThrow('HAP -70402')
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { WaterDetectorDevice } from '../../src/devices/genericDevice'
|
|
4
|
+
|
|
5
|
+
const mockLogger = { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }
|
|
6
|
+
|
|
7
|
+
const mockApi = {
|
|
8
|
+
hap: {
|
|
9
|
+
HapStatusError: class HapStatusError extends Error {
|
|
10
|
+
constructor(public status: number) {
|
|
11
|
+
super(`HAP status ${status}`)
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
HAPStatus: {
|
|
15
|
+
SERVICE_COMMUNICATION_FAILURE: -70402,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('water detector device leak state', () => {
|
|
21
|
+
it('warms leak state and serves LeakDetected synchronously from cache', async () => {
|
|
22
|
+
let reads = 0
|
|
23
|
+
const device = new WaterDetectorDevice({ id: 'wd1', type: 'waterdetector' }, { log: mockLogger })
|
|
24
|
+
device.getState = async () => {
|
|
25
|
+
reads += 1
|
|
26
|
+
return { waterLeakDetected: true }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await device.init()
|
|
30
|
+
const leakDetected = device.createHAPAccessory(mockApi).services[0].characteristics.LeakDetected.get()
|
|
31
|
+
|
|
32
|
+
expect(leakDetected).toBe(1)
|
|
33
|
+
expect(leakDetected).not.toBeInstanceOf(Promise)
|
|
34
|
+
expect(reads).toBe(1)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('throws a HAP communication failure instead of blocking when no leak state is cached', () => {
|
|
38
|
+
const device = new WaterDetectorDevice({ id: 'wd1', type: 'waterdetector' }, { log: mockLogger })
|
|
39
|
+
device.getState = async () => ({ waterLeakDetected: false })
|
|
40
|
+
|
|
41
|
+
expect(() => device.createHAPAccessory(mockApi).services[0].characteristics.LeakDetected.get()).toThrow('HAP status -70402')
|
|
42
|
+
})
|
|
43
|
+
})
|