@switchbot/homebridge-switchbot 5.2.3-beta.1 → 5.2.4
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/dist/SwitchBotMatterPlatform.d.ts +0 -2
- package/dist/SwitchBotMatterPlatform.d.ts.map +1 -1
- package/dist/SwitchBotMatterPlatform.js +9 -28
- package/dist/SwitchBotMatterPlatform.js.map +1 -1
- package/dist/devices/genericDevice.d.ts +2 -0
- package/dist/devices/genericDevice.d.ts.map +1 -1
- package/dist/devices/genericDevice.js +60 -10
- package/dist/devices/genericDevice.js.map +1 -1
- package/dist/homebridge-ui/public/js/app.js +2 -2
- package/dist/homebridge-ui/public/js/app.js.map +2 -2
- package/dist/homebridge-ui/public/js/modals.js +2 -2
- package/dist/homebridge-ui/public/js/modals.js.map +1 -1
- package/dist/homebridge-ui/public/js/modals.ts +2 -2
- package/dist/homebridge-ui/switchbotClient.js +7 -41
- package/dist/homebridge-ui/switchbotClient.js.map +1 -1
- package/dist/switchbotClient.d.ts +6 -15
- package/dist/switchbotClient.d.ts.map +1 -1
- package/dist/switchbotClient.js +7 -41
- package/dist/switchbotClient.js.map +1 -1
- package/docs/variables/default.html +1 -1
- package/package.json +2 -2
- package/src/SwitchBotMatterPlatform.ts +8 -27
- package/src/devices/genericDevice.ts +57 -10
- package/src/homebridge-ui/public/js/modals.ts +2 -2
- package/src/switchbotClient.ts +8 -52
- package/test/device/curtain-ble-position.spec.ts +34 -0
- package/.github/npm-version-script-esm.js +0 -103
- package/test/client/switchbot-client-keys.spec.ts +0 -59
- package/test/platform/matter-registration.spec.ts +0 -68
|
@@ -67,10 +67,19 @@ export class GenericDevice extends DeviceBase {
|
|
|
67
67
|
if (!this.log) {
|
|
68
68
|
throw new Error('Device requires a logger (Homebridge logger) in opts or cfg')
|
|
69
69
|
}
|
|
70
|
-
// BLE
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
// If BLE encryptionKey/keyId provided, set on node-switchbot device instance if possible
|
|
71
|
+
if (opts.encryptionKey && this.client && typeof this.client.devices?.get === 'function') {
|
|
72
|
+
try {
|
|
73
|
+
const dev = this.client.devices.get(opts.id)
|
|
74
|
+
if (dev && typeof dev.setKey === 'function') {
|
|
75
|
+
dev.setKey({
|
|
76
|
+
encryptionKey: opts.encryptionKey,
|
|
77
|
+
keyId: opts.keyId || undefined,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
// ignore if device not found or setKey not available
|
|
82
|
+
}
|
|
74
83
|
}
|
|
75
84
|
// BLE polling config: allow override via opts.blePollingEnabled/blePollIntervalMs or cfg.blePollingEnabled/blePollIntervalMs
|
|
76
85
|
this._blePollingEnabled = opts?.blePollingEnabled ?? cfg?.blePollingEnabled ?? true
|
|
@@ -616,6 +625,8 @@ export class CurtainDevice extends GenericDevice {
|
|
|
616
625
|
private lastTargetPosition = 0
|
|
617
626
|
private positionState = 2
|
|
618
627
|
private preferLocalPositionUntil = 0
|
|
628
|
+
private lastAdvertisement?: object
|
|
629
|
+
private positionBeforeCommand?: number
|
|
619
630
|
|
|
620
631
|
private clampHomeKitPosition(position: number): number {
|
|
621
632
|
return Math.max(0, Math.min(100, Math.round(Number(position))))
|
|
@@ -630,6 +641,21 @@ export class CurtainDevice extends GenericDevice {
|
|
|
630
641
|
}
|
|
631
642
|
|
|
632
643
|
private async getPositionForHomeKit(): Promise<number> {
|
|
644
|
+
const device = this.client?.getCachedDevice?.(this.opts.id)
|
|
645
|
+
if (device?.hasBLE?.()) {
|
|
646
|
+
const advertisement = device.getInfo?.()?.bleServiceData
|
|
647
|
+
if (advertisement && advertisement !== this.lastAdvertisement) {
|
|
648
|
+
this.lastAdvertisement = advertisement
|
|
649
|
+
if (typeof advertisement.position === 'number' && Number.isFinite(advertisement.position)) {
|
|
650
|
+
const position = this.toHomeKitPosition(advertisement.position)
|
|
651
|
+
if (this.positionBeforeCommand === undefined || position !== this.positionBeforeCommand) {
|
|
652
|
+
this.lastKnownPosition = position
|
|
653
|
+
this.lastTargetPosition = position
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return this.lastKnownPosition
|
|
658
|
+
}
|
|
633
659
|
if (Date.now() < this.preferLocalPositionUntil) {
|
|
634
660
|
return this.lastKnownPosition
|
|
635
661
|
}
|
|
@@ -637,6 +663,9 @@ export class CurtainDevice extends GenericDevice {
|
|
|
637
663
|
this.getState(),
|
|
638
664
|
new Promise(resolve => setTimeout(resolve, 1000)),
|
|
639
665
|
])
|
|
666
|
+
if (Date.now() < this.preferLocalPositionUntil) {
|
|
667
|
+
return this.lastKnownPosition
|
|
668
|
+
}
|
|
640
669
|
if (typeof state?.position === 'number') {
|
|
641
670
|
this.lastKnownPosition = this.toHomeKitPosition(Number(state.position))
|
|
642
671
|
this.lastTargetPosition = this.lastKnownPosition
|
|
@@ -652,7 +681,8 @@ export class CurtainDevice extends GenericDevice {
|
|
|
652
681
|
characteristics: {
|
|
653
682
|
CurrentPosition: {
|
|
654
683
|
get: async () => {
|
|
655
|
-
|
|
684
|
+
void this.getPositionForHomeKit().catch(() => {})
|
|
685
|
+
return this.lastKnownPosition
|
|
656
686
|
},
|
|
657
687
|
},
|
|
658
688
|
PositionState: {
|
|
@@ -660,17 +690,34 @@ export class CurtainDevice extends GenericDevice {
|
|
|
660
690
|
},
|
|
661
691
|
TargetPosition: {
|
|
662
692
|
get: async () => {
|
|
663
|
-
|
|
693
|
+
void this.getPositionForHomeKit().catch(() => {})
|
|
664
694
|
return this.lastTargetPosition
|
|
665
695
|
},
|
|
666
696
|
set: async (v: any) => {
|
|
667
697
|
const position = this.clampHomeKitPosition(Number(v))
|
|
698
|
+
const bleDevice = this.client?.getCachedDevice?.(this.opts.id)
|
|
699
|
+
if (bleDevice?.hasBLE?.()) {
|
|
700
|
+
await this.getPositionForHomeKit()
|
|
701
|
+
}
|
|
702
|
+
const previousPosition = this.lastKnownPosition
|
|
668
703
|
this.lastTargetPosition = position
|
|
669
704
|
this.positionState = position > this.lastKnownPosition ? 1 : position < this.lastKnownPosition ? 0 : 2
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
705
|
+
try {
|
|
706
|
+
const result = await this.setState({ position: this.toSwitchBotPosition(position) })
|
|
707
|
+
if (result === false || result?.success === false) {
|
|
708
|
+
throw new Error(result?.reason ?? 'Curtain position command failed')
|
|
709
|
+
}
|
|
710
|
+
this.lastKnownPosition = position
|
|
711
|
+
if (bleDevice?.hasBLE?.()) {
|
|
712
|
+
this.positionBeforeCommand = previousPosition
|
|
713
|
+
}
|
|
714
|
+
this.preferLocalPositionUntil = Date.now() + 30000
|
|
715
|
+
} catch (error) {
|
|
716
|
+
this.lastTargetPosition = this.lastKnownPosition
|
|
717
|
+
throw error
|
|
718
|
+
} finally {
|
|
719
|
+
this.positionState = 2
|
|
720
|
+
}
|
|
674
721
|
},
|
|
675
722
|
refreshAfterSet: ['CurrentPosition', 'TargetPosition', 'PositionState'],
|
|
676
723
|
},
|
|
@@ -149,7 +149,7 @@ export async function importDiscoveredDevice(device: any): Promise<ImportDiscove
|
|
|
149
149
|
['auto', 'ble', 'openapi'].forEach((val) => {
|
|
150
150
|
const opt = document.createElement('option')
|
|
151
151
|
opt.value = val
|
|
152
|
-
opt.text = val
|
|
152
|
+
opt.text = val === 'ble' ? 'BLE' : val === 'openapi' ? 'OpenAPI' : 'Auto'
|
|
153
153
|
opt.selected = (device.connectionPreference || 'auto') === val
|
|
154
154
|
connectionPrefSelect.appendChild(opt)
|
|
155
155
|
})
|
|
@@ -568,7 +568,7 @@ export async function editDevice(device: any): Promise<void> {
|
|
|
568
568
|
;['auto', 'ble', 'openapi'].forEach((val) => {
|
|
569
569
|
const opt = document.createElement('option')
|
|
570
570
|
opt.value = val
|
|
571
|
-
opt.text = val
|
|
571
|
+
opt.text = val === 'ble' ? 'BLE' : val === 'openapi' ? 'OpenAPI' : 'Auto'
|
|
572
572
|
opt.selected = (device.connectionPreference || 'auto') === val
|
|
573
573
|
connectionPrefSelect.appendChild(opt)
|
|
574
574
|
})
|
package/src/switchbotClient.ts
CHANGED
|
@@ -12,28 +12,14 @@ export interface ISwitchBotClient {
|
|
|
12
12
|
getDevice: (id: string) => Promise<any>
|
|
13
13
|
getDevices: () => Promise<any[]>
|
|
14
14
|
setDeviceState: (id: string, body: any) => Promise<any>
|
|
15
|
-
setDeviceKey?: (id: string, key: DeviceKey) => void
|
|
16
15
|
destroy: () => Promise<void>
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
/** BLE encryption key for a device (relay switches, locks, air purifiers, bulbs...) */
|
|
20
|
-
export interface DeviceKey {
|
|
21
|
-
encryptionKey: string
|
|
22
|
-
keyId?: string
|
|
23
|
-
}
|
|
24
|
-
|
|
25
18
|
/**
|
|
26
19
|
* Thin wrapper around node-switchbot v4.0.0+
|
|
27
20
|
* Leverages upstream resilience features (retry, circuit breaker, connection intelligence)
|
|
28
21
|
* while maintaining plugin-specific features like write debouncing and OpenAPI fallback.
|
|
29
22
|
*/
|
|
30
|
-
const APPLIED_KEY = Symbol('appliedDeviceKey')
|
|
31
|
-
|
|
32
|
-
/** Compare ids case-insensitively and without MAC separators */
|
|
33
|
-
function normalizeDeviceId(id: string): string {
|
|
34
|
-
return String(id).replace(/[:-]/g, '').toLowerCase()
|
|
35
|
-
}
|
|
36
|
-
|
|
37
23
|
export class SwitchBotClient implements ISwitchBotClient {
|
|
38
24
|
private cfg: SwitchBotPluginConfig
|
|
39
25
|
private client: SwitchBot | null = null
|
|
@@ -41,8 +27,6 @@ export class SwitchBotClient implements ISwitchBotClient {
|
|
|
41
27
|
private discoveryCacheTtlMs = 30_000
|
|
42
28
|
private lastDiscoveryAt = 0
|
|
43
29
|
private discoveryInFlight?: Promise<any[]>
|
|
44
|
-
/** Keys by device id, applied to the node-switchbot device whenever it is looked up */
|
|
45
|
-
private deviceKeys = new Map<string, DeviceKey>()
|
|
46
30
|
private logger: import('homebridge').Logger
|
|
47
31
|
private pendingWrites: Map<string, { timer: any, body: any, resolvers: Array<{ resolve: (v: any) => void, reject: (e: any) => void }> }> = new Map()
|
|
48
32
|
|
|
@@ -90,41 +74,7 @@ export class SwitchBotClient implements ISwitchBotClient {
|
|
|
90
74
|
}
|
|
91
75
|
}
|
|
92
76
|
|
|
93
|
-
/**
|
|
94
|
-
* Set the BLE encryption key for a device. The node-switchbot device may not exist yet (it is
|
|
95
|
-
* created by discovery), so the key is kept and applied each time the device is looked up.
|
|
96
|
-
*/
|
|
97
|
-
setDeviceKey(id: string, key: DeviceKey): void {
|
|
98
|
-
if (!id || !key?.encryptionKey) {
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
this.deviceKeys.set(normalizeDeviceId(id), key)
|
|
102
|
-
this.applyDeviceKey(this.getManagedDevice(id))
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
private applyDeviceKey(device: any): any {
|
|
106
|
-
if (!device || typeof device.setKey !== 'function' || this.deviceKeys.size === 0) {
|
|
107
|
-
return device
|
|
108
|
-
}
|
|
109
|
-
const info = typeof device.getInfo === 'function' ? device.getInfo() : device
|
|
110
|
-
const ids = [device.id, info?.id, info?.mac, info?.address, info?.deviceId].filter(Boolean).map(normalizeDeviceId)
|
|
111
|
-
const key = ids.map(i => this.deviceKeys.get(i)).find(Boolean)
|
|
112
|
-
if (key && device[APPLIED_KEY] !== key) {
|
|
113
|
-
try {
|
|
114
|
-
device.setKey({ encryptionKey: key.encryptionKey, keyId: key.keyId || undefined })
|
|
115
|
-
device[APPLIED_KEY] = key
|
|
116
|
-
} catch (e: any) {
|
|
117
|
-
this.logger?.warn?.(`[${device.id}] Invalid BLE encryption key: ${e?.message ?? e}`)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return device
|
|
121
|
-
}
|
|
122
|
-
|
|
123
77
|
async getDevice(id: string): Promise<any> {
|
|
124
|
-
return this.applyDeviceKey(await this.findDevice(id))
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
private async findDevice(id: string): Promise<any> {
|
|
128
78
|
if (this.client) {
|
|
129
79
|
try {
|
|
130
80
|
const fromManager = this.getManagedDevice(id)
|
|
@@ -159,12 +109,18 @@ export class SwitchBotClient implements ISwitchBotClient {
|
|
|
159
109
|
throw new SwitchbotOperationError('No SwitchBot client available', 'no_client')
|
|
160
110
|
}
|
|
161
111
|
|
|
112
|
+
getCachedDevice(id: string): any {
|
|
113
|
+
return this.getManagedDevice(id)
|
|
114
|
+
}
|
|
115
|
+
|
|
162
116
|
async getDevices(): Promise<any[]> {
|
|
163
117
|
if (this.client) {
|
|
164
118
|
try {
|
|
165
119
|
const fromManager = this.getManagedDevices()
|
|
166
|
-
|
|
167
|
-
|
|
120
|
+
if (fromManager.length > 0) {
|
|
121
|
+
return fromManager
|
|
122
|
+
}
|
|
123
|
+
return await this.ensureDiscovered(false)
|
|
168
124
|
} catch (e) {
|
|
169
125
|
this.logger?.warn?.('Client getDevices failed:', e)
|
|
170
126
|
throw e
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { CurtainDevice } from '../../src/devices/genericDevice'
|
|
4
|
+
|
|
5
|
+
describe('curtain BLE position', () => {
|
|
6
|
+
it('keeps a commanded position when an old advertisement reappears', async () => {
|
|
7
|
+
let advertisement = { position: 39 }
|
|
8
|
+
const nodeDevice = {
|
|
9
|
+
hasBLE: () => true,
|
|
10
|
+
getInfo: () => ({ bleServiceData: advertisement }),
|
|
11
|
+
}
|
|
12
|
+
const client = { getCachedDevice: () => nodeDevice }
|
|
13
|
+
const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }
|
|
14
|
+
const curtain = new CurtainDevice({ id: 'curtain', type: 'Curtain', log, blePollingEnabled: false }, { _client: client, log } as any)
|
|
15
|
+
const getState = vi.spyOn(curtain, 'getState')
|
|
16
|
+
vi.spyOn(curtain, 'setState').mockResolvedValue(true)
|
|
17
|
+
const characteristics = curtain.createHAPAccessory({}).services[0].characteristics
|
|
18
|
+
|
|
19
|
+
expect(await characteristics.CurrentPosition.get()).toBe(61)
|
|
20
|
+
await characteristics.TargetPosition.set(69)
|
|
21
|
+
expect(await characteristics.CurrentPosition.get()).toBe(69)
|
|
22
|
+
|
|
23
|
+
advertisement = { position: 39 }
|
|
24
|
+
expect(await characteristics.CurrentPosition.get()).toBe(69)
|
|
25
|
+
expect(await characteristics.TargetPosition.get()).toBe(69)
|
|
26
|
+
|
|
27
|
+
advertisement = { position: 31 }
|
|
28
|
+
expect(await characteristics.CurrentPosition.get()).toBe(69)
|
|
29
|
+
|
|
30
|
+
advertisement = { position: 39 }
|
|
31
|
+
expect(await characteristics.CurrentPosition.get()).toBe(69)
|
|
32
|
+
expect(getState).not.toHaveBeenCalled()
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
#!/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* This scripts queries the npm registry to pull out the latest version for a given tag.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import assert from 'node:assert'
|
|
8
|
-
import child_process from 'node:child_process'
|
|
9
|
-
import fs from 'node:fs'
|
|
10
|
-
import process from 'node:process'
|
|
11
|
-
|
|
12
|
-
const BRANCH_VERSION_PATTERN = /^([A-Z]+)-(\d+\.\d+\.\d+)$/i
|
|
13
|
-
|
|
14
|
-
// Load the contents of the package.json file
|
|
15
|
-
const packageJSON = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
|
16
|
-
|
|
17
|
-
const refArgument = process.argv[2]
|
|
18
|
-
const tagArgument = process.argv[3] || 'latest'
|
|
19
|
-
|
|
20
|
-
if (!refArgument) {
|
|
21
|
-
console.error('ref argument is missing')
|
|
22
|
-
console.error('Usage: npm-version-script-esm.js <ref> [tag]')
|
|
23
|
-
process.exit(1)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Queries the NPM registry for the latest version for the provided base version and tag.
|
|
28
|
-
* If the tag is latest, then the base version is returned if it exists. For other tags, the latest
|
|
29
|
-
* version found for that base version and tag is returned.
|
|
30
|
-
* @param baseVersion The base version to query for, e.g. 2.0.0
|
|
31
|
-
* @param tag The tag to query for, e.g. beta or latest
|
|
32
|
-
* @returns {string} Returns the version, or '' if not found
|
|
33
|
-
*/
|
|
34
|
-
function getTagVersionFromNpm(baseVersion, tag) {
|
|
35
|
-
try {
|
|
36
|
-
return JSON.parse(child_process.execSync(`npm info ${packageJSON.name} versions --json`).toString('utf8').trim())
|
|
37
|
-
// find all published versions for this base version and tag
|
|
38
|
-
.filter(v => tag === 'latest'
|
|
39
|
-
? v === baseVersion
|
|
40
|
-
: v.startsWith(`${baseVersion}-${tag}.`))
|
|
41
|
-
.reduce((_, current) => current, '') // choose the last as they're sorted in ascending order, or '' if there are none
|
|
42
|
-
} catch (e) {
|
|
43
|
-
console.error(`Failed to query the npm registry for the latest version for tag: ${tag}`, e)
|
|
44
|
-
// throw e;
|
|
45
|
-
return ''
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function desiredTargetVersion(ref) {
|
|
50
|
-
// ref is a GitHub action ref string
|
|
51
|
-
if (ref.startsWith('refs/pull/')) {
|
|
52
|
-
throw new Error('The version script was executed inside a PR!')
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
assert(ref.startsWith('refs/heads/'))
|
|
56
|
-
const branchName = ref.slice('refs/heads/'.length)
|
|
57
|
-
|
|
58
|
-
const results = branchName.match(BRANCH_VERSION_PATTERN)
|
|
59
|
-
if (results !== null) {
|
|
60
|
-
if (results[1] !== tagArgument) {
|
|
61
|
-
console.warn(`The base branch name (${results[1]}) differs from the tag name ${tagArgument}`)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return results[2]
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
throw new Error(`Malformed branch name for ref: ${ref}. Can't derive the base version. Use a branch name like: beta-x.x.x or alpha-x.x.x`)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// derive the base version from the branch ref
|
|
71
|
-
const baseVersion = desiredTargetVersion(refArgument)
|
|
72
|
-
|
|
73
|
-
// query the npm registry for the latest version of the provided tag name
|
|
74
|
-
const latestReleasedVersion = getTagVersionFromNpm(baseVersion, tagArgument) // e.g. 0.7.0-beta.12
|
|
75
|
-
|
|
76
|
-
let publishTag
|
|
77
|
-
|
|
78
|
-
if (latestReleasedVersion) {
|
|
79
|
-
console.warn(`Latest published version for ${baseVersion} with tag ${tagArgument} is ${latestReleasedVersion}`)
|
|
80
|
-
publishTag = latestReleasedVersion // set this released beta or alpha to be incremented
|
|
81
|
-
} else {
|
|
82
|
-
console.warn(`No published versions for ${baseVersion} with tag ${tagArgument}`)
|
|
83
|
-
publishTag = baseVersion // start off with a new beta or alpha version
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (packageJSON.version !== publishTag) {
|
|
87
|
-
// report the change for CI
|
|
88
|
-
console.warn(`Changing version in package.json from ${packageJSON.version} to ${publishTag}`)
|
|
89
|
-
|
|
90
|
-
// save the package.json
|
|
91
|
-
// JSON.stringify does not end with a newline, but npm writes both of these
|
|
92
|
-
// files with one. Leaving it off makes the file fail an eol-last lint rule,
|
|
93
|
-
// which breaks any repo whose prepublishOnly lints the whole tree.
|
|
94
|
-
packageJSON.version = publishTag
|
|
95
|
-
fs.writeFileSync('package.json', `${JSON.stringify(packageJSON, null, 2)}\n`)
|
|
96
|
-
|
|
97
|
-
// perform the same change to the package-lock.json
|
|
98
|
-
const packageLockJSON = JSON.parse(fs.readFileSync('package-lock.json', 'utf8'))
|
|
99
|
-
packageLockJSON.version = publishTag
|
|
100
|
-
fs.writeFileSync('package-lock.json', `${JSON.stringify(packageLockJSON, null, 2)}\n`)
|
|
101
|
-
} else {
|
|
102
|
-
console.warn(`Leaving version in package.json at ${packageJSON.version}`)
|
|
103
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { SwitchBotClient } from '../../src/switchbotClient'
|
|
4
|
-
|
|
5
|
-
const logger = { info: () => {}, warn: vi.fn(), error: () => {}, debug: () => {} }
|
|
6
|
-
|
|
7
|
-
function client(devices: any[]) {
|
|
8
|
-
const c = new SwitchBotClient({ logger } as any)
|
|
9
|
-
;(c as any).client = {
|
|
10
|
-
discover: vi.fn(async () => devices),
|
|
11
|
-
devices: { get: (id: string) => devices.find(d => d.id === id), list: () => devices },
|
|
12
|
-
}
|
|
13
|
-
return c
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function device(id: string, mac?: string) {
|
|
17
|
-
return { id, setKey: vi.fn(), getInfo: () => ({ id, mac }) }
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// genericDevice used to call client.devices.get(), which the plugin's client wrapper does not have,
|
|
21
|
-
// so BLE encryption keys were never passed to node-switchbot.
|
|
22
|
-
describe('switchBotClient device keys', () => {
|
|
23
|
-
it('applies a key set before the device is discovered', async () => {
|
|
24
|
-
const devices: any[] = []
|
|
25
|
-
const c = client(devices)
|
|
26
|
-
c.setDeviceKey('AA:BB:CC:DD:EE:FF', { encryptionKey: '0'.repeat(32), keyId: 'ab' })
|
|
27
|
-
const relay = device('aabbccddeeff', 'AA:BB:CC:DD:EE:FF')
|
|
28
|
-
devices.push(relay)
|
|
29
|
-
await c.getDevice('aabbccddeeff')
|
|
30
|
-
expect(relay.setKey).toHaveBeenCalledWith({ encryptionKey: '0'.repeat(32), keyId: 'ab' })
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('applies a key immediately when the device already exists, and only once', async () => {
|
|
34
|
-
const relay = device('relay-1')
|
|
35
|
-
const c = client([relay])
|
|
36
|
-
c.setDeviceKey('relay-1', { encryptionKey: '1'.repeat(32), keyId: '01' })
|
|
37
|
-
await c.getDevice('relay-1')
|
|
38
|
-
await c.getDevices()
|
|
39
|
-
expect(relay.setKey).toHaveBeenCalledOnce()
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('logs and continues when node-switchbot rejects the key', async () => {
|
|
43
|
-
const relay = device('relay-2')
|
|
44
|
-
relay.setKey.mockImplementation(() => {
|
|
45
|
-
throw new Error('bad key')
|
|
46
|
-
})
|
|
47
|
-
const c = client([relay])
|
|
48
|
-
c.setDeviceKey('relay-2', { encryptionKey: 'nope' })
|
|
49
|
-
await expect(c.getDevice('relay-2')).resolves.toBe(relay)
|
|
50
|
-
expect(logger.warn).toHaveBeenCalled()
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('leaves devices without setKey (node-switchbot 5.x) alone', async () => {
|
|
54
|
-
const plain = { id: 'plain' }
|
|
55
|
-
const c = client([plain])
|
|
56
|
-
c.setDeviceKey('plain', { encryptionKey: '2'.repeat(32) })
|
|
57
|
-
await expect(c.getDevice('plain')).resolves.toBe(plain)
|
|
58
|
-
})
|
|
59
|
-
})
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { SwitchBotMatterPlatform } from '../../src/SwitchBotMatterPlatform.js'
|
|
4
|
-
|
|
5
|
-
function makeApi(failFor?: string) {
|
|
6
|
-
const registered = new Set<string>()
|
|
7
|
-
const matter = {
|
|
8
|
-
uuid: { generate: (id: string) => `uuid-${id}` },
|
|
9
|
-
deviceTypes: {},
|
|
10
|
-
registerPlatformAccessories: vi.fn(async (_p: string, _n: string, accessories: any[]) => {
|
|
11
|
-
for (const a of accessories) {
|
|
12
|
-
if (a.UUID === failFor) {
|
|
13
|
-
throw new Error('boom')
|
|
14
|
-
}
|
|
15
|
-
if (registered.has(a.UUID)) {
|
|
16
|
-
throw new Error(`Matter accessory with UUID ${a.UUID} is already registered`)
|
|
17
|
-
}
|
|
18
|
-
registered.add(a.UUID)
|
|
19
|
-
}
|
|
20
|
-
}),
|
|
21
|
-
updatePlatformAccessories: vi.fn(async () => {}),
|
|
22
|
-
unregisterPlatformAccessories: vi.fn(async () => {}),
|
|
23
|
-
}
|
|
24
|
-
return { api: { matter, on: vi.fn() } as any, matter }
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function created(name: string) {
|
|
28
|
-
return {
|
|
29
|
-
created: { createAccessory: async () => ({ name, clusters: { onOff: { onOff: false } } }) },
|
|
30
|
-
d: { id: name, name },
|
|
31
|
-
type: 'plug',
|
|
32
|
-
useMatter: true,
|
|
33
|
-
matterAvailable: true,
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), success: vi.fn() } as any
|
|
38
|
-
|
|
39
|
-
describe('matter accessory registration', () => {
|
|
40
|
-
it('updates instead of re-registering on a second load (config reload)', async () => {
|
|
41
|
-
const { api, matter } = makeApi()
|
|
42
|
-
const platform = new SwitchBotMatterPlatform(log, { platform: 'SwitchBot' } as any, api)
|
|
43
|
-
await platform.registerMatterAccessories([created('a')])
|
|
44
|
-
await platform.registerMatterAccessories([created('a')])
|
|
45
|
-
expect(matter.registerPlatformAccessories).toHaveBeenCalledTimes(1)
|
|
46
|
-
expect(matter.updatePlatformAccessories).toHaveBeenCalledTimes(1)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('registers cache-restored accessories so handlers attach', async () => {
|
|
50
|
-
const { api, matter } = makeApi()
|
|
51
|
-
const platform = new SwitchBotMatterPlatform(log, { platform: 'SwitchBot' } as any, api)
|
|
52
|
-
platform.configureMatterAccessory({ UUID: 'uuid-a', displayName: 'a', context: { deviceId: 'a' }, _restoredFromCache: true })
|
|
53
|
-
await platform.registerMatterAccessories([created('a')])
|
|
54
|
-
expect(matter.registerPlatformAccessories).toHaveBeenCalledTimes(1)
|
|
55
|
-
expect(matter.registerPlatformAccessories.mock.calls[0][2][0].handlers).toBeDefined()
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('one failing accessory does not stop the others', async () => {
|
|
59
|
-
const { api, matter } = makeApi('uuid-a')
|
|
60
|
-
const platform = new SwitchBotMatterPlatform(log, { platform: 'SwitchBot' } as any, api)
|
|
61
|
-
await platform.registerMatterAccessories([created('a'), created('b')])
|
|
62
|
-
const uuids = matter.registerPlatformAccessories.mock.calls.map(c => c[2][0].UUID)
|
|
63
|
-
expect(uuids).toEqual(['uuid-a', 'uuid-b'])
|
|
64
|
-
// 'a' failed, so it is registered (not updated) on the next attempt
|
|
65
|
-
await platform.registerMatterAccessories([created('a')])
|
|
66
|
-
expect(matter.registerPlatformAccessories).toHaveBeenCalledTimes(3)
|
|
67
|
-
})
|
|
68
|
-
})
|