@switchbot/homebridge-switchbot 5.2.2-beta.0 → 5.2.2-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +4 -0
- package/dist/SwitchBotHAPPlatform.d.ts.map +1 -1
- package/dist/SwitchBotHAPPlatform.js +8 -6
- package/dist/SwitchBotHAPPlatform.js.map +1 -1
- package/dist/cacheRepair.d.ts +19 -0
- package/dist/cacheRepair.d.ts.map +1 -0
- package/dist/cacheRepair.js +68 -0
- package/dist/cacheRepair.js.map +1 -0
- package/dist/deviceCommandMapper.d.ts +19 -5
- package/dist/deviceCommandMapper.d.ts.map +1 -1
- package/dist/deviceCommandMapper.js +132 -312
- package/dist/deviceCommandMapper.js.map +1 -1
- package/dist/devices/genericDevice.d.ts.map +1 -1
- package/dist/devices/genericDevice.js +8 -2
- package/dist/devices/genericDevice.js.map +1 -1
- package/dist/homebridge-ui/deviceCommandMapper.js +132 -312
- package/dist/homebridge-ui/deviceCommandMapper.js.map +1 -1
- package/dist/homebridge-ui/switchbotClient.js +30 -9
- package/dist/homebridge-ui/switchbotClient.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/switchbotClient.d.ts +1 -0
- package/dist/switchbotClient.d.ts.map +1 -1
- package/dist/switchbotClient.js +30 -9
- package/dist/switchbotClient.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +21 -1
- package/dist/utils.js.map +1 -1
- package/docs/variables/default.html +1 -1
- package/package.json +2 -2
- package/src/SwitchBotHAPPlatform.ts +10 -6
- package/src/cacheRepair.ts +79 -0
- package/src/deviceCommandMapper.ts +142 -317
- package/src/devices/genericDevice.ts +8 -2
- package/src/index.ts +3 -0
- package/src/switchbotClient.ts +30 -9
- package/src/utils.ts +21 -1
- package/test/client/node-switchbot-commands.spec.ts +85 -0
- package/test/client/switchbot-client-debounce.spec.ts +17 -0
- package/test/client/switchbot-client-discovery.spec.ts +40 -0
- package/test/device/genericDevice-blepoll.spec.ts +42 -0
- package/test/platform/hap-cache-restart.spec.ts +112 -0
- package/test/utils/utils.spec.ts +26 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { SwitchBot } from 'node-switchbot'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { hsToRgb, toKelvin, toRgb } from '../../src/deviceCommandMapper'
|
|
5
|
+
import { SwitchBotClient } from '../../src/switchbotClient'
|
|
6
|
+
|
|
7
|
+
const logger = { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }
|
|
8
|
+
|
|
9
|
+
// Drive real node-switchbot devices (OpenAPI only) through the plugin client and record what
|
|
10
|
+
// reaches the OpenAPI. Commands used to be looked up by device type string, and most real types
|
|
11
|
+
// ("Plug Mini (US)", "Strip Light", "Lock Pro", ...) had no entry, so they failed as unsupported.
|
|
12
|
+
async function send(deviceType: string, ...bodies: any[]) {
|
|
13
|
+
const switchbot = new SwitchBot({ enableBLE: false, token: 'token', secret: 'secret', enableRetry: false })
|
|
14
|
+
const api = switchbot.getAPIClient()!
|
|
15
|
+
vi.spyOn(api, 'getDevices').mockResolvedValue({
|
|
16
|
+
deviceList: [{ deviceId: 'DEV', deviceName: 'Test', deviceType, enableCloudService: true, hubDeviceId: 'HUB' }],
|
|
17
|
+
infraredRemoteList: [],
|
|
18
|
+
} as any)
|
|
19
|
+
const sendCommand = vi.spyOn(api, 'sendCommand').mockResolvedValue({ statusCode: 100, message: 'success', body: {} } as any)
|
|
20
|
+
const client = new SwitchBotClient({ logger, writeDebounceMs: 0 } as any)
|
|
21
|
+
;(client as any).client = switchbot
|
|
22
|
+
for (const body of bodies) {
|
|
23
|
+
await client.setDeviceState('DEV', body)
|
|
24
|
+
}
|
|
25
|
+
return sendCommand.mock.calls.map(([, command, parameter]) => [command, parameter])
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('commands reach node-switchbot devices', () => {
|
|
29
|
+
it.each(['Plug Mini (US)', 'Plug Mini (JP)', 'Plug', 'Relay Switch 1', 'Strip Light', 'Color Bulb', 'Ceiling Light'])('%s turns on and off', async (type) => {
|
|
30
|
+
expect(await send(type, { command: 'turnOn' }, { command: 'turnOff' })).toEqual([['turnOn', undefined], ['turnOff', undefined]])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('lock Pro locks and unlocks', async () => {
|
|
34
|
+
const calls = await send('Smart Lock Pro', { command: 'lock' }, { command: 'unlock' })
|
|
35
|
+
expect(calls.map(([command]) => command)).toEqual(['lock', 'unlock'])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// Brightness arrives as a string; color as "hue,saturation"; color temperature in mireds.
|
|
39
|
+
it('color Bulb converts brightness, color and color temperature', async () => {
|
|
40
|
+
const calls = await send(
|
|
41
|
+
'Color Bulb',
|
|
42
|
+
{ command: 'setBrightness', parameter: '40' },
|
|
43
|
+
{ command: 'setColor', parameter: '120,100' },
|
|
44
|
+
{ command: 'setColorTemperature', parameter: '250' },
|
|
45
|
+
)
|
|
46
|
+
expect(calls).toEqual([
|
|
47
|
+
['setBrightness', 40],
|
|
48
|
+
['setColor', '0:255:0'],
|
|
49
|
+
['setColorTemperature', 4000],
|
|
50
|
+
])
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('curtain and Blind Tilt set their position', async () => {
|
|
54
|
+
expect(await send('Curtain3', { command: 'setPosition', parameter: '30' })).toEqual([['setPosition', '0,ff,30']])
|
|
55
|
+
expect(await send('Blind Tilt', { command: 'setPosition', parameter: '50' })).toEqual([['setPosition', 'down;100']])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('a vacuum starts and docks', async () => {
|
|
59
|
+
expect(await send('K10+', { command: 'start' }, { command: 'dock' })).toEqual([['start', undefined], ['dock', undefined]])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('an unsupported command is rejected', async () => {
|
|
63
|
+
await expect(send('Meter', { command: 'turnOn' })).rejects.toThrow('Unsupported command')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('parameter conversion', () => {
|
|
68
|
+
it('converts mireds to kelvin and passes kelvin through', () => {
|
|
69
|
+
expect(toKelvin(140)).toBe(6500)
|
|
70
|
+
expect(toKelvin(500)).toBe(2700)
|
|
71
|
+
expect(toKelvin(4000)).toBe(4000)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('converts hue and saturation to RGB', () => {
|
|
75
|
+
expect(hsToRgb(0, 100)).toEqual([255, 0, 0])
|
|
76
|
+
expect(hsToRgb(240, 100)).toEqual([0, 0, 255])
|
|
77
|
+
expect(hsToRgb(0, 0)).toEqual([255, 255, 255])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('accepts hex and r:g:b colors', () => {
|
|
81
|
+
expect(toRgb('#FF8000')).toEqual([255, 128, 0])
|
|
82
|
+
expect(toRgb('10:20:30')).toEqual([10, 20, 30])
|
|
83
|
+
expect(() => toRgb('nope')).toThrow('Unrecognised color')
|
|
84
|
+
})
|
|
85
|
+
})
|
|
@@ -32,4 +32,21 @@ describe('switchBotClient debounce', () => {
|
|
|
32
32
|
expect(doSetDeviceState).toHaveBeenCalledWith('device-1', { command: 'turnOn' })
|
|
33
33
|
expect(doSetDeviceState).toHaveBeenCalledWith('device-2', { command: 'turnOff' })
|
|
34
34
|
})
|
|
35
|
+
|
|
36
|
+
// HomeKit writes On and Brightness together when a light is turned on at a level. Debouncing by
|
|
37
|
+
// device alone kept only the last body, so turnOn was dropped while both callers saw success.
|
|
38
|
+
it('keeps different operations on the same device', async () => {
|
|
39
|
+
const client = new SwitchBotClient({ logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} } } as any)
|
|
40
|
+
const doSetDeviceState = vi.spyOn(client as any, '_doSetDeviceState').mockResolvedValue({ status: 'success' })
|
|
41
|
+
|
|
42
|
+
await Promise.all([
|
|
43
|
+
client.setDeviceState('bulb', { command: 'turnOn' }),
|
|
44
|
+
client.setDeviceState('bulb', { command: 'setBrightness', parameter: 40 }),
|
|
45
|
+
client.setDeviceState('bulb', { command: 'setBrightness', parameter: 50 }),
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
expect(doSetDeviceState).toHaveBeenCalledTimes(2)
|
|
49
|
+
expect(doSetDeviceState).toHaveBeenCalledWith('bulb', { command: 'turnOn' })
|
|
50
|
+
expect(doSetDeviceState).toHaveBeenCalledWith('bulb', { command: 'setBrightness', parameter: 50 })
|
|
51
|
+
})
|
|
35
52
|
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { SwitchBotClient } from '../../src/switchbotClient'
|
|
4
|
+
|
|
5
|
+
function client(devices: any[] = []) {
|
|
6
|
+
const c = new SwitchBotClient({ logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} } } as any)
|
|
7
|
+
const discover = vi.fn(async () => {
|
|
8
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
9
|
+
return devices
|
|
10
|
+
})
|
|
11
|
+
;(c as any).client = { discover, devices: { get: () => undefined, list: () => devices } }
|
|
12
|
+
return { c, discover }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// A configured device that is offline or out of range is not in the device list. Every lookup
|
|
16
|
+
// forced a full discovery (BLE scan plus an OpenAPI getDevices call), and concurrent lookups each
|
|
17
|
+
// started their own.
|
|
18
|
+
describe('switchBotClient rediscovery for a missing device', () => {
|
|
19
|
+
it('shares one discovery between concurrent lookups', async () => {
|
|
20
|
+
const { c, discover } = client([{ id: 'present' }])
|
|
21
|
+
await Promise.all([c.getDevice('missing-1'), c.getDevice('missing-2'), c.getDevice('missing-3')])
|
|
22
|
+
expect(discover).toHaveBeenCalledOnce()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('does not rediscover again within the cache period', async () => {
|
|
26
|
+
const { c, discover } = client([{ id: 'present' }])
|
|
27
|
+
await c.getDevice('missing')
|
|
28
|
+
await c.getDevice('missing')
|
|
29
|
+
await c.getDevice('missing')
|
|
30
|
+
expect(discover).toHaveBeenCalledOnce()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('still finds a device that appears after a rediscovery', async () => {
|
|
34
|
+
const devices: any[] = []
|
|
35
|
+
const { c, discover } = client(devices)
|
|
36
|
+
devices.push({ id: 'late' })
|
|
37
|
+
await expect(c.getDevice('late')).resolves.toEqual({ id: 'late' })
|
|
38
|
+
expect(discover).toHaveBeenCalledOnce()
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -45,3 +45,45 @@ describe('genericDevice BLE polling config', () => {
|
|
|
45
45
|
expect((device as any)._blePollIntervalMs).toBe(600000)
|
|
46
46
|
})
|
|
47
47
|
})
|
|
48
|
+
|
|
49
|
+
// "BLE polling" called getState() for every device every 10 minutes, even with BLE disabled or for
|
|
50
|
+
// a cloud-only device. getState() then reads status over the OpenAPI, outside the plugin's daily
|
|
51
|
+
// API budget and on top of the normal refresh, which can use up the 10,000 calls a day.
|
|
52
|
+
describe('genericDevice BLE polling only polls over BLE', () => {
|
|
53
|
+
function device(cfg: Record<string, unknown>, hasBLE: boolean | undefined) {
|
|
54
|
+
const nodeDevice = hasBLE === undefined ? undefined : { hasBLE: () => hasBLE }
|
|
55
|
+
const client = { devices: { get: () => nodeDevice }, getDevice: vi.fn(async () => nodeDevice) }
|
|
56
|
+
const d = new GenericDevice({ id: 'dev', type: 'Unknown', log: mockLogger, blePollIntervalMs: 60_000 }, { log: mockLogger, _client: client, ...cfg } as any)
|
|
57
|
+
const getState = vi.spyOn(d, 'getState').mockResolvedValue({})
|
|
58
|
+
return { d, getState }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
it('does not start when BLE is disabled', () => {
|
|
62
|
+
const { d } = device({ enableBLE: false }, true)
|
|
63
|
+
expect((d as any)._blePollTimer).toBeNull()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('skips devices without a BLE connection', async () => {
|
|
67
|
+
vi.useFakeTimers()
|
|
68
|
+
try {
|
|
69
|
+
const { d, getState } = device({}, false)
|
|
70
|
+
await vi.advanceTimersByTimeAsync(60_000)
|
|
71
|
+
expect(getState).not.toHaveBeenCalled()
|
|
72
|
+
await d.destroy()
|
|
73
|
+
} finally {
|
|
74
|
+
vi.useRealTimers()
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('polls devices that have BLE', async () => {
|
|
79
|
+
vi.useFakeTimers()
|
|
80
|
+
try {
|
|
81
|
+
const { d, getState } = device({}, true)
|
|
82
|
+
await vi.advanceTimersByTimeAsync(60_000)
|
|
83
|
+
expect(getState).toHaveBeenCalledOnce()
|
|
84
|
+
await d.destroy()
|
|
85
|
+
} finally {
|
|
86
|
+
vi.useRealTimers()
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { mkdtempSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import * as hap from '@homebridge/hap-nodejs'
|
|
6
|
+
import { Characteristic, Service, uuid } from '@homebridge/hap-nodejs'
|
|
7
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
8
|
+
|
|
9
|
+
// Homebridge exports only the PlatformAccessory type. This test needs the real class, because its
|
|
10
|
+
// serialize/deserialize are what write and read the accessory cache.
|
|
11
|
+
// eslint-disable-next-line antfu/no-import-dist, antfu/no-import-node-modules-by-path
|
|
12
|
+
import { PlatformAccessory } from '../../node_modules/homebridge/dist/platformAccessory.js'
|
|
13
|
+
import { repairCachedAccessories } from '../../src/cacheRepair.js'
|
|
14
|
+
import { createDevice } from '../../src/deviceFactory.js'
|
|
15
|
+
import { PLATFORM_NAME, PLUGIN_NAME } from '../../src/settings.js'
|
|
16
|
+
import { SwitchBotHAPPlatform } from '../../src/SwitchBotHAPPlatform.js'
|
|
17
|
+
import { normalizeTypeForMatter } from '../../src/utils.js'
|
|
18
|
+
|
|
19
|
+
const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }
|
|
20
|
+
const INFO = Service.AccessoryInformation.UUID
|
|
21
|
+
|
|
22
|
+
async function startOnce(cache: any[] | undefined, devices: { id: string, type: string, name: string }[], mutate?: (json: any) => void) {
|
|
23
|
+
// Homebridge sets these when it registers an accessory; serialize() requires them.
|
|
24
|
+
const register = (accs: any[]) => accs.forEach((a) => {
|
|
25
|
+
a._associatedPlugin = PLUGIN_NAME
|
|
26
|
+
a._associatedPlatform = PLATFORM_NAME
|
|
27
|
+
})
|
|
28
|
+
const api = { hap: { Service, Characteristic, uuid }, platformAccessory: PlatformAccessory, registerPlatformAccessories: vi.fn((_p, _q, accs) => register(accs)), updatePlatformAccessories: vi.fn() }
|
|
29
|
+
const platform = Object.create(SwitchBotHAPPlatform.prototype) as any
|
|
30
|
+
platform.api = api
|
|
31
|
+
platform.log = log
|
|
32
|
+
platform.config = { devices: devices.map(d => ({ deviceId: d.id, configDeviceType: d.type })) }
|
|
33
|
+
platform.hapPushTargets = new Map()
|
|
34
|
+
platform.accessories = new Map()
|
|
35
|
+
for (const json of cache ?? []) {
|
|
36
|
+
mutate?.(json)
|
|
37
|
+
const acc: any = PlatformAccessory.deserialize(json) // the call that throws in #1424
|
|
38
|
+
platform.accessories.set(acc.UUID, acc)
|
|
39
|
+
}
|
|
40
|
+
const list = []
|
|
41
|
+
for (const d of devices) {
|
|
42
|
+
const type = normalizeTypeForMatter(d.type)
|
|
43
|
+
const _client = { getDeviceStatus: vi.fn(async () => ({})), setDeviceState: vi.fn(async () => ({ success: true })), getDevice: vi.fn(async () => undefined) }
|
|
44
|
+
const created = await createDevice({ id: d.id, type, deviceType: d.type, name: d.name, log } as any, { log, devices: platform.config.devices, _client } as any, false)
|
|
45
|
+
list.push({ created, d, type })
|
|
46
|
+
}
|
|
47
|
+
log.warn.mockClear()
|
|
48
|
+
await platform.registerHAPAccessories(list)
|
|
49
|
+
expect(log.warn).not.toHaveBeenCalled()
|
|
50
|
+
return [...platform.accessories.values()].map((a: any) => PlatformAccessory.serialize(a))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('hAP accessory cache across restarts (#1424)', () => {
|
|
54
|
+
const devices = [
|
|
55
|
+
{ id: 'EA552551B763', type: 'Curtain3', name: 'Curtain3-1' },
|
|
56
|
+
{ id: 'D83BDA1173CA', type: 'Plug', name: 'Plug-1' },
|
|
57
|
+
]
|
|
58
|
+
const hasInfo = (cache: any[]) => cache.every(a => a.services.some((s: any) => s.UUID === INFO))
|
|
59
|
+
|
|
60
|
+
it('survives restarts', async () => {
|
|
61
|
+
let cache = await startOnce(undefined, devices)
|
|
62
|
+
expect(cache).toHaveLength(2)
|
|
63
|
+
for (let i = 0; i < 3; i++) cache = await startOnce(cache, devices)
|
|
64
|
+
expect(hasInfo(cache)).toBe(true)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// HAP restores a service whose class name was not stored as a plain `Service`. The plugin
|
|
68
|
+
// used to match services by class name, removed AccessoryInformation, and the next start crashed.
|
|
69
|
+
it('survives restarts with a cache whose services have no constructorName', async () => {
|
|
70
|
+
let cache = await startOnce(undefined, devices)
|
|
71
|
+
for (let i = 0; i < 3; i++) {
|
|
72
|
+
cache = await startOnce(cache, devices, json => json.services.forEach((s: any) => delete s.constructorName))
|
|
73
|
+
}
|
|
74
|
+
expect(hasInfo(cache)).toBe(true)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('repairs a cache that is already missing AccessoryInformation, so the bridge can start', async () => {
|
|
78
|
+
const dir = mkdtempSync(join(tmpdir(), 'switchbot-cache-'))
|
|
79
|
+
const good = await startOnce(undefined, devices)
|
|
80
|
+
const broken = good.map(a => ({ ...a, services: a.services.filter((s: any) => s.UUID !== INFO) }))
|
|
81
|
+
const otherPlugin = { ...broken[0], plugin: 'homebridge-other', UUID: uuid.generate('other') }
|
|
82
|
+
const file = join(dir, 'cachedAccessories.0EF8A6716C95')
|
|
83
|
+
writeFileSync(file, JSON.stringify([...broken, otherPlugin]))
|
|
84
|
+
expect(() => PlatformAccessory.deserialize(broken[0])).toThrow('getCharacteristic')
|
|
85
|
+
|
|
86
|
+
const messages: string[] = []
|
|
87
|
+
const api: any = { hap, user: { cachedAccessoryPath: () => dir } }
|
|
88
|
+
const results = repairCachedAccessories(api, m => messages.push(m))
|
|
89
|
+
|
|
90
|
+
expect(results).toEqual([{ file, repaired: ['Curtain3-1', 'Plug-1'] }])
|
|
91
|
+
expect(messages[0]).toContain('Repaired 2 cached accessories')
|
|
92
|
+
expect(readdirSync(dir)).toContain('cachedAccessories.0EF8A6716C95.switchbot-repair.bak')
|
|
93
|
+
const repaired = JSON.parse(readFileSync(file, 'utf8'))
|
|
94
|
+
expect(repaired[2]).toEqual(otherPlugin) // other plugins' entries are left alone
|
|
95
|
+
let cache = await startOnce(repaired.slice(0, 2), devices)
|
|
96
|
+
cache = await startOnce(cache, devices)
|
|
97
|
+
expect(hasInfo(cache)).toBe(true)
|
|
98
|
+
// A second run finds nothing to do and does not rewrite the file.
|
|
99
|
+
expect(repairCachedAccessories(api)).toEqual([])
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('ignores missing directories, backups and unreadable files', () => {
|
|
103
|
+
const dir = mkdtempSync(join(tmpdir(), 'switchbot-cache-'))
|
|
104
|
+
writeFileSync(join(dir, '.cachedAccessories.bak'), JSON.stringify([{ plugin: PLUGIN_NAME, services: [] }]))
|
|
105
|
+
writeFileSync(join(dir, 'cachedAccessories'), '{not json')
|
|
106
|
+
const messages: string[] = []
|
|
107
|
+
expect(repairCachedAccessories({ hap, user: { cachedAccessoryPath: () => dir } } as any, m => messages.push(m))).toEqual([])
|
|
108
|
+
expect(messages).toHaveLength(1)
|
|
109
|
+
expect(repairCachedAccessories({ hap, user: { cachedAccessoryPath: () => join(dir, 'missing') } } as any)).toEqual([])
|
|
110
|
+
expect(repairCachedAccessories({} as any)).toEqual([])
|
|
111
|
+
})
|
|
112
|
+
})
|
package/test/utils/utils.spec.ts
CHANGED
|
@@ -29,6 +29,32 @@ describe('utils', () => {
|
|
|
29
29
|
expect(result).not.toBe(input)
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
+
// The config from #1424: credentials and options in the v4 layout, read by a v5 plugin that
|
|
33
|
+
// only looked at openApiToken/openApiSecret/enableBLE, so the OpenAPI was never configured.
|
|
34
|
+
it('normalizeConfig reads v4 credentials and options when the v5 keys are missing', () => {
|
|
35
|
+
const devices = [{ deviceId: 'EA552551B763', configDeviceType: 'Curtain3' }]
|
|
36
|
+
const result = normalizeConfig({
|
|
37
|
+
platform: 'SwitchBot',
|
|
38
|
+
credentials: { token: 'TOKEN', secret: 'SECRET', notice: 'Keep your Token & Secret a secret!' },
|
|
39
|
+
options: { BLE: false, discoverBLE: false, refreshRate: 180, logging: 'standard', devices },
|
|
40
|
+
} as any)
|
|
41
|
+
expect(result).toMatchObject({ openApiToken: 'TOKEN', openApiSecret: 'SECRET', enableBLE: false, openApiRefreshRate: 180, devices })
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('normalizeConfig prefers the v5 keys over v4 ones', () => {
|
|
45
|
+
const devices = [{ deviceId: 'A' }]
|
|
46
|
+
const result = normalizeConfig({
|
|
47
|
+
openApiToken: 'NEW',
|
|
48
|
+
openApiSecret: 'NEWSECRET',
|
|
49
|
+
enableBLE: true,
|
|
50
|
+
openApiRefreshRate: 600,
|
|
51
|
+
devices,
|
|
52
|
+
credentials: { token: 'OLD', secret: 'OLDSECRET' },
|
|
53
|
+
options: { BLE: false, refreshRate: 60, devices: [{ deviceId: 'B' }] },
|
|
54
|
+
} as any)
|
|
55
|
+
expect(result).toMatchObject({ openApiToken: 'NEW', openApiSecret: 'NEWSECRET', enableBLE: true, openApiRefreshRate: 600, devices })
|
|
56
|
+
})
|
|
57
|
+
|
|
32
58
|
it('createPlatformProxy instantiates MatterPlatform if available and enabled', () => {
|
|
33
59
|
const HAP = vi.fn()
|
|
34
60
|
const Matter = vi.fn()
|