@switchbot/homebridge-switchbot 5.0.0-beta.134 → 5.0.0-beta.136

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.
Files changed (93) hide show
  1. package/config.schema.json +0 -11
  2. package/dist/deviceFactory.d.ts +3 -1
  3. package/dist/deviceFactory.d.ts.map +1 -1
  4. package/dist/deviceFactory.js +3 -18
  5. package/dist/deviceFactory.js.map +1 -1
  6. package/dist/devices/genericDevice.d.ts +1 -1
  7. package/dist/devices/genericDevice.d.ts.map +1 -1
  8. package/dist/devices/genericDevice.js +79 -159
  9. package/dist/devices/genericDevice.js.map +1 -1
  10. package/dist/homebridge-ui/public/index.html +107 -124
  11. package/dist/homebridge-ui/public/js/api.d.ts +1 -4
  12. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  13. package/dist/homebridge-ui/public/js/api.js +1 -7
  14. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  15. package/dist/homebridge-ui/public/js/api.ts +1 -8
  16. package/dist/homebridge-ui/public/js/app.js +201 -90
  17. package/dist/homebridge-ui/public/js/app.js.map +2 -2
  18. package/dist/homebridge-ui/public/js/app.ts +5 -6
  19. package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -1
  20. package/dist/homebridge-ui/public/js/credentials.js +1 -1
  21. package/dist/homebridge-ui/public/js/credentials.js.map +1 -1
  22. package/dist/homebridge-ui/public/js/credentials.ts +7 -3
  23. package/dist/homebridge-ui/public/js/devices-delete.js +1 -1
  24. package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -1
  25. package/dist/homebridge-ui/public/js/devices-delete.ts +1 -1
  26. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
  27. package/dist/homebridge-ui/public/js/devices.js +0 -1
  28. package/dist/homebridge-ui/public/js/devices.js.map +1 -1
  29. package/dist/homebridge-ui/public/js/devices.ts +0 -2
  30. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
  31. package/dist/homebridge-ui/public/js/discovery.js +6 -3
  32. package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
  33. package/dist/homebridge-ui/public/js/discovery.ts +9 -3
  34. package/dist/homebridge-ui/public/js/logger.d.ts +1 -0
  35. package/dist/homebridge-ui/public/js/logger.d.ts.map +1 -1
  36. package/dist/homebridge-ui/public/js/logger.js +8 -3
  37. package/dist/homebridge-ui/public/js/logger.js.map +1 -1
  38. package/dist/homebridge-ui/public/js/logger.ts +12 -6
  39. package/dist/homebridge-ui/public/js/modals.d.ts +4 -0
  40. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
  41. package/dist/homebridge-ui/public/js/modals.js +152 -59
  42. package/dist/homebridge-ui/public/js/modals.js.map +1 -1
  43. package/dist/homebridge-ui/public/js/modals.ts +187 -60
  44. package/dist/homebridge-ui/public/js/render.d.ts +3 -0
  45. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
  46. package/dist/homebridge-ui/public/js/render.js +63 -24
  47. package/dist/homebridge-ui/public/js/render.js.map +1 -1
  48. package/dist/homebridge-ui/public/js/render.ts +102 -51
  49. package/dist/homebridge-ui/utils/logger.js +1 -1
  50. package/dist/homebridge-ui/utils/logger.js.map +1 -1
  51. package/dist/platform.d.ts.map +1 -1
  52. package/dist/platform.js +40 -32
  53. package/dist/platform.js.map +1 -1
  54. package/dist/switchbotClient.d.ts +5 -5
  55. package/dist/switchbotClient.d.ts.map +1 -1
  56. package/dist/switchbotClient.js +21 -12
  57. package/dist/switchbotClient.js.map +1 -1
  58. package/docs/variables/default.html +1 -1
  59. package/package.json +1 -1
  60. package/scripts/build-ui.js +4 -3
  61. package/scripts/generate-matter-maps.js +34 -17
  62. package/scripts/sync-device-types.mjs +4 -1
  63. package/src/deviceFactory.ts +3 -18
  64. package/src/devices/genericDevice.ts +77 -164
  65. package/src/homebridge-ui/public/index.html +107 -124
  66. package/src/homebridge-ui/public/js/api.ts +1 -8
  67. package/src/homebridge-ui/public/js/app.ts +5 -6
  68. package/src/homebridge-ui/public/js/credentials.ts +7 -3
  69. package/src/homebridge-ui/public/js/devices-delete.ts +1 -1
  70. package/src/homebridge-ui/public/js/devices.ts +0 -2
  71. package/src/homebridge-ui/public/js/discovery.ts +9 -3
  72. package/src/homebridge-ui/public/js/logger.ts +12 -6
  73. package/src/homebridge-ui/public/js/modals.ts +187 -60
  74. package/src/homebridge-ui/public/js/render.ts +102 -51
  75. package/src/homebridge-ui/utils/logger.ts +1 -1
  76. package/src/platform.ts +40 -31
  77. package/src/switchbotClient.ts +42 -27
  78. package/test/accessory-restore.spec.ts +9 -8
  79. package/test/device-mapping.spec.ts +4 -3
  80. package/test/deviceFactory.spec.ts +4 -3
  81. package/test/e2e/run-e2e.spec.ts +6 -10
  82. package/test/fan-swing.spec.ts +7 -6
  83. package/test/helpers/matter-harness.ts +18 -7
  84. package/test/lock-users.spec.ts +5 -5
  85. package/test/matter-childbridge.spec.ts +10 -7
  86. package/test/matter-descriptors.spec.ts +8 -8
  87. package/test/matter-device-state.spec.ts +52 -36
  88. package/test/matter-integration.spec.ts +53 -6
  89. package/test/platform.integration.spec.ts +7 -5
  90. package/test/switchbot-client-debounce.spec.ts +36 -40
  91. package/test/switchbot-client-openapi.spec.ts +22 -15
  92. package/test/switchbotClient.spec.ts +4 -3
  93. package/test/utils.spec.ts +2 -1
@@ -22,29 +22,25 @@ describe('e2e scripts (manual-run harness)', () => {
22
22
  { envVar: 'LOCK_ACCESSORY_ID', script: 'lock-e2e.sh' },
23
23
  ]
24
24
 
25
+ const logger = console
25
26
  for (const s of scriptsToRun) {
26
27
  if (!process.env[s.envVar]) {
27
- // skip script if no accessory id configured
28
- // eslint-disable-next-line no-console
29
- console.log(`Skipping ${s.script} because ${s.envVar} not set`)
28
+ logger.info(`Skipping ${s.script} because ${s.envVar} not set`)
30
29
  continue
31
30
  }
32
31
 
33
32
  const scriptPath = path.join(scriptsDir, s.script)
34
- // Forward environment so scripts can read HB_URL / HB_TOKEN / <ACCESSORY_ID>
35
33
  const env = { ...process.env }
36
34
 
37
35
  try {
38
- // eslint-disable-next-line no-console
39
- console.log(`Running ${s.script}...`)
36
+ logger.info(`Running ${s.script}...`)
40
37
  const { stdout, stderr } = await execFileP('bash', [scriptPath], { env })
41
- // eslint-disable-next-line no-console
42
- console.log(stdout)
38
+ logger.info(stdout)
43
39
  if (stderr) {
44
- console.error(stderr)
40
+ logger.error(stderr)
45
41
  }
46
42
  } catch (e: any) {
47
- // bubble up as test failure
43
+ logger.error(`Script ${s.script} failed: ${e.message}`)
48
44
  throw new Error(`Script ${s.script} failed: ${e.message}`)
49
45
  }
50
46
  }
@@ -1,19 +1,20 @@
1
- import { describe, it, expect, vi } from 'vitest'
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
2
3
  import { FanDevice } from '../src/devices/genericDevice'
3
4
 
4
- describe('Fan swing commands', () => {
5
+ describe('fan swing commands', () => {
5
6
  it('sends setSwing on boolean swing', async () => {
6
7
  const mockClient = { setDeviceState: vi.fn(async (id: string, body: any) => ({ id, body })) }
7
- const d = new FanDevice({ id: 'fan1', type: 'fan', name: 'F' }, { _client: mockClient } as any)
8
+ const d = new FanDevice({ id: 'fan1', type: 'fan', name: 'F' }, { _client: mockClient, log: console } as any)
8
9
  await d.init()
9
- const res = await d.setState({ swing: true })
10
+ await d.setState({ swing: true })
10
11
  expect(mockClient.setDeviceState).toHaveBeenCalled()
11
12
  expect((mockClient.setDeviceState as any).mock.calls[0][1]).toEqual({ command: 'setSwing', parameter: 'on', commandType: 'command' })
12
13
  })
13
14
 
14
15
  it('sends setSwing with angle', async () => {
15
16
  const mockClient = { setDeviceState: vi.fn(async (id: string, body: any) => ({ id, body })) }
16
- const d = new FanDevice({ id: 'fan2', type: 'fan', name: 'F2' }, { _client: mockClient } as any)
17
+ const d = new FanDevice({ id: 'fan2', type: 'fan', name: 'F2' }, { _client: mockClient, log: console } as any)
17
18
  await d.init()
18
19
  await d.setState({ swingAngle: 45 })
19
20
  expect((mockClient.setDeviceState as any).mock.calls[0][1]).toEqual({ command: 'setSwing', parameter: '45', commandType: 'command' })
@@ -21,7 +22,7 @@ describe('Fan swing commands', () => {
21
22
 
22
23
  it('sends setSwing with mode', async () => {
23
24
  const mockClient = { setDeviceState: vi.fn(async (id: string, body: any) => ({ id, body })) }
24
- const d = new FanDevice({ id: 'fan3', type: 'fan', name: 'F3' }, { _client: mockClient } as any)
25
+ const d = new FanDevice({ id: 'fan3', type: 'fan', name: 'F3' }, { _client: mockClient, log: console } as any)
25
26
  await d.init()
26
27
  await d.setState({ swingMode: 'vertical' })
27
28
  expect((mockClient.setDeviceState as any).mock.calls[0][1]).toEqual({ command: 'setSwing', parameter: 'vertical', commandType: 'command' })
@@ -30,16 +30,27 @@ export function makeFakeApiWithMatter(matterApi?: any) {
30
30
 
31
31
  export function makeFakeApiWithoutMatter() {
32
32
  // Provide a minimal platformAccessory constructor and registerPlatformAccessories
33
- function PlatformAccessory(name: string, uuid: string) {
33
+ interface IPlatformAccessory {
34
+ displayName: string
35
+ UUID: string
36
+ services: any[]
37
+ context: Record<string, any>
38
+ getService: (type: any) => any
39
+ addService: (type: any) => any
40
+ }
41
+ function PlatformAccessory(this: IPlatformAccessory, name: string, uuid: string) {
34
42
  this.displayName = name
35
43
  this.UUID = uuid
36
44
  this.services = []
37
- this.getService = (type: any) => this.services.find((s: any) => s.type === type)
38
- this.addService = (type: any) => {
39
- const s: any = { type, characteristics: {} }
40
- this.services.push(s)
41
- return s
42
- }
45
+ this.context = {}
46
+ }
47
+ PlatformAccessory.prototype.getService = function (this: IPlatformAccessory, type: any) {
48
+ return this.services.find((s: any) => s.type === type)
49
+ }
50
+ PlatformAccessory.prototype.addService = function (this: IPlatformAccessory, type: any) {
51
+ const s: any = { type, characteristics: {} }
52
+ this.services.push(s)
53
+ return s
43
54
  }
44
55
 
45
56
  return {
@@ -4,7 +4,7 @@ import { LockDevice } from '../src/devices/genericDevice'
4
4
  describe('Lock user management', () => {
5
5
  it('sets lock pin via pin field', async () => {
6
6
  const mockClient = { setDeviceState: vi.fn(async (id: string, body: any) => ({ id, body })) }
7
- const d = new LockDevice({ id: 'lock1', type: 'lock', name: 'L1' }, { _client: mockClient } as any)
7
+ const d = new LockDevice({ id: 'lock1', type: 'lock', name: 'L1' }, { _client: mockClient, log: console } as any)
8
8
  await d.init()
9
9
  await d.setState({ pin: '1234' })
10
10
  expect((mockClient.setDeviceState as any).mock.calls[0][1]).toEqual({ command: 'setLockPin', parameter: '1234', commandType: 'command' })
@@ -12,7 +12,7 @@ describe('Lock user management', () => {
12
12
 
13
13
  it('adds a user code with addUser action', async () => {
14
14
  const mockClient = { setDeviceState: vi.fn(async (id: string, body: any) => ({ id, body })) }
15
- const d = new LockDevice({ id: 'lock2', type: 'lock', name: 'L2' }, { _client: mockClient } as any)
15
+ const d = new LockDevice({ id: 'lock2', type: 'lock', name: 'L2' }, { _client: mockClient, log: console } as any)
16
16
  await d.init()
17
17
  await d.setState({ action: 'addUser', user: 'user1', pin: '9999' })
18
18
  expect((mockClient.setDeviceState as any).mock.calls[0][1]).toEqual({ command: 'addUserCode', parameter: 'user1:9999', commandType: 'command' })
@@ -20,7 +20,7 @@ describe('Lock user management', () => {
20
20
 
21
21
  it('removes a user code with removeUser action', async () => {
22
22
  const mockClient = { setDeviceState: vi.fn(async (id: string, body: any) => ({ id, body })) }
23
- const d = new LockDevice({ id: 'lock3', type: 'lock', name: 'L3' }, { _client: mockClient } as any)
23
+ const d = new LockDevice({ id: 'lock3', type: 'lock', name: 'L3' }, { _client: mockClient, log: console } as any)
24
24
  await d.init()
25
25
  await d.setState({ action: 'removeUser', user: 'user1' })
26
26
  expect((mockClient.setDeviceState as any).mock.calls[0][1]).toEqual({ command: 'removeUserCode', parameter: 'user1', commandType: 'command' })
@@ -28,7 +28,7 @@ describe('Lock user management', () => {
28
28
 
29
29
  it('lists users with listUsers action', async () => {
30
30
  const mockClient = { setDeviceState: vi.fn(async (id: string, body: any) => ({ id, body })) }
31
- const d = new LockDevice({ id: 'lock4', type: 'lock', name: 'L4' }, { _client: mockClient } as any)
31
+ const d = new LockDevice({ id: 'lock4', type: 'lock', name: 'L4' }, { _client: mockClient, log: console } as any)
32
32
  await d.init()
33
33
  await d.setState({ action: 'listUsers' })
34
34
  expect((mockClient.setDeviceState as any).mock.calls[0][1]).toEqual({ command: 'listUsers', parameter: 'default', commandType: 'command' })
@@ -36,7 +36,7 @@ describe('Lock user management', () => {
36
36
 
37
37
  it('unlocks with pin using unlockWithPin action', async () => {
38
38
  const mockClient = { setDeviceState: vi.fn(async (id: string, body: any) => ({ id, body })) }
39
- const d = new LockDevice({ id: 'lock5', type: 'lock', name: 'L5' }, { _client: mockClient } as any)
39
+ const d = new LockDevice({ id: 'lock5', type: 'lock', name: 'L5' }, { _client: mockClient, log: console } as any)
40
40
  await d.init()
41
41
  await d.setState({ action: 'unlockWithPin', pin: '5555' })
42
42
  expect((mockClient.setDeviceState as any).mock.calls[0][1]).toEqual({ command: 'unlockWithPin', parameter: '5555', commandType: 'command' })
@@ -1,5 +1,5 @@
1
- import { describe, it, expect, beforeEach } from 'vitest'
2
- import SwitchBotPlatform from '../src/platform'
1
+ import { describe, expect, it } from 'vitest'
2
+
3
3
  import { createPlatformProxy } from '../src/utils'
4
4
 
5
5
  // This is a lightweight integration-style unit test that verifies the
@@ -14,23 +14,26 @@ describe('platform integration (Matter)', () => {
14
14
  constructor(public log: any, public cfg: any, public api: any) {}
15
15
  async registerMatterAccessories(accessories: any[]) {
16
16
  this.registered.push(...accessories)
17
- return
18
17
  }
18
+
19
19
  // simulate configureAccessory hook for restored matter accessories
20
- configureMatterAccessory(accessory: any) {
20
+ configureMatterAccessory() {
21
21
  // no-op
22
22
  }
23
+
23
24
  // lightweight loadDevices implementation that uses an injected discover hook if present
24
25
  async loadDevices() {
25
26
  const devices = (this.cfg?.devices) || (this as any)._discoverDevices ? await (this as any)._discoverDevices() : []
26
27
  // Simulate preparing descriptors and immediately registering them
27
28
  const serialized = devices.map((d: any) => ({ id: d.id ?? d.deviceId ?? d, protocol: 'matter', name: d.name }))
28
- if (serialized.length > 0) await this.registerMatterAccessories(serialized)
29
+ if (serialized.length > 0) {
30
+ await this.registerMatterAccessories(serialized)
31
+ }
29
32
  }
30
33
  }
31
34
 
32
35
  // Create proxy that would choose the fake matter platform when preferred
33
- const Proxy = createPlatformProxy(function HAP() {}, FakeMatterPlatform)
36
+ const Proxy = createPlatformProxy(() => {}, FakeMatterPlatform)
34
37
  const matterApi = {
35
38
  isMatterAvailable: () => true,
36
39
  isMatterEnabled: () => true,
@@ -53,7 +56,7 @@ describe('platform integration (Matter)', () => {
53
56
  expect(registered).toHaveProperty('protocol', 'matter')
54
57
  expect(registered).toHaveProperty('id', device.id)
55
58
  } else {
56
- throw new Error('platform.loadDevices not implemented')
59
+ throw new TypeError('platform.loadDevices not implemented')
57
60
  }
58
61
  })
59
62
  })
@@ -13,7 +13,7 @@ describe('matter descriptors and translations (per-device)', () => {
13
13
  calls.push({ id, body })
14
14
  return { ok: true }
15
15
  } }
16
- const d = new FanDevice({ id: 'f1', type: 'fan' } as any, { _client: fakeClient } as any)
16
+ const d = new FanDevice({ id: 'f1', type: 'fan' } as any, { _client: fakeClient, log: console } as any)
17
17
 
18
18
  // Oscillate
19
19
  await d.setState({ oscillate: true })
@@ -36,7 +36,7 @@ describe('matter descriptors and translations (per-device)', () => {
36
36
  calls.push(body)
37
37
  return { ok: true }
38
38
  } }
39
- const d = new LightDevice({ id: 'l1', type: 'light' } as any, { _client: fakeClient } as any)
39
+ const d = new LightDevice({ id: 'l1', type: 'light' } as any, { _client: fakeClient, log: console } as any)
40
40
 
41
41
  // color temperature (mired)
42
42
  await d.setState({ colorTemperature: 300 })
@@ -59,18 +59,18 @@ describe('matter descriptors and translations (per-device)', () => {
59
59
  calls.push(body)
60
60
  return { ok: true }
61
61
  } }
62
- const d = new CurtainDevice({ id: 'c1', type: 'curtain' } as any, { _client: fakeClient } as any)
62
+ const d = new CurtainDevice({ id: 'c1', type: 'curtain' } as any, { _client: fakeClient, log: console } as any)
63
63
 
64
64
  // Setting position via HAP-style setState
65
65
  await d.setState({ position: 25 })
66
66
  expect(calls.pop()?.command).toBe('setPosition')
67
67
 
68
- // Matter descriptor write: call targetPosition write handler
68
+ // Matter descriptor write: call targetPositionLiftPercent100ths write handler
69
69
  const matter = await d.createMatterAccessory({})
70
70
  const cluster = matter.clusters.find((c: any) => c.type === 'WindowCovering')
71
- // targetPosition attribute exists and has a write function
72
- expect(cluster.attributes.targetPosition.write).toBeTypeOf('function')
73
- await cluster.attributes.targetPosition.write(80)
71
+ // targetPositionLiftPercent100ths attribute exists and has a write function
72
+ expect(cluster.attributes.targetPositionLiftPercent100ths.write).toBeTypeOf('function')
73
+ await cluster.attributes.targetPositionLiftPercent100ths.write(8000)
74
74
  expect(calls.pop()?.command).toBe('setPosition')
75
75
  })
76
76
 
@@ -80,7 +80,7 @@ describe('matter descriptors and translations (per-device)', () => {
80
80
  calls.push(body)
81
81
  return { ok: true }
82
82
  } }
83
- const d = new LockDevice({ id: 'lock1', type: 'lock' } as any, { _client: fakeClient } as any)
83
+ const d = new LockDevice({ id: 'lock1', type: 'lock' } as any, { _client: fakeClient, log: console } as any)
84
84
 
85
85
  // addUser via setState
86
86
  await d.setState({ action: 'addUser', user: 'u1', pin: '1234' })
@@ -1,20 +1,24 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { LightDevice, FanDevice, CurtainDevice, LockDevice, HumidifierMatterDevice } from '../src/devices/genericDevice'
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { CurtainDevice, FanDevice, HumidifierMatterDevice, LightDevice, LockDevice } from '../src/devices/genericDevice'
3
4
  import { makeFakeApiWithMatter } from './helpers/matter-harness'
4
5
 
5
6
  const api = makeFakeApiWithMatter()
6
7
 
7
- describe('Matter device state translations', () => {
8
- it('Light: reads and writes map to setState/getState', async () => {
9
- const device = new LightDevice({ id: 'l-1', type: 'light', name: 'L1' }, {})
8
+ describe('matter device state translations', () => {
9
+ it('light: reads and writes map to setState/getState', async () => {
10
+ const device = new LightDevice({ id: 'l-1', type: 'light', name: 'L1' }, { log: console })
10
11
  let lastSet: any = null
11
12
  device.getState = async () => ({ on: true, brightness: 75, hue: 120, saturation: 50, colorTemperature: 350 })
12
- device.setState = async (c: any) => { lastSet = c; return { success: true } }
13
+ device.setState = async (c: any) => {
14
+ lastSet = c
15
+ return { success: true }
16
+ }
13
17
 
14
18
  const desc = await device.createMatterAccessory(api)
15
19
  const levelCluster = desc.clusters.find((c: any) => c.clusterId === 0x0008 || c.type === 'LevelControl')
16
20
  expect(levelCluster).toBeDefined()
17
- const levelAttr = levelCluster.attributes.currentLevel || levelCluster.attributes["0x0000"] || Object.values(levelCluster.attributes)[0]
21
+ const levelAttr = levelCluster.attributes.currentLevel || levelCluster.attributes['0x0000'] || Object.values(levelCluster.attributes)[0]
18
22
  const read = await levelAttr.read()
19
23
  expect(read).toBe(75)
20
24
 
@@ -23,11 +27,14 @@ describe('Matter device state translations', () => {
23
27
  expect(lastSet).toMatchObject({ brightness: 30 })
24
28
  })
25
29
 
26
- it('Fan: reads speed and writes speed/oscillation', async () => {
27
- const device = new FanDevice({ id: 'f-1', type: 'fan', name: 'F1' }, {})
28
- let last: any = null
30
+ it('fan: reads speed and writes speed/oscillation', async () => {
31
+ const device = new FanDevice({ id: 'f-1', type: 'fan', name: 'F1' }, { log: console })
32
+ let lastFan: any = null
29
33
  device.getState = async () => ({ on: false, speed: 45, oscillating: true, swingMode: 'auto' })
30
- device.setState = async (c: any) => { last = c; return { success: true } }
34
+ device.setState = async (c: any) => {
35
+ lastFan = c
36
+ return { success: true }
37
+ }
31
38
 
32
39
  const desc = await device.createMatterAccessory(api)
33
40
  const fanCluster = desc.clusters.find((c: any) => c.clusterId === 0x0202 || c.type === 'FanControl')
@@ -37,65 +44,74 @@ describe('Matter device state translations', () => {
37
44
  expect(speed).toBe(45)
38
45
 
39
46
  await speedAttr.write(80)
40
- expect(last).toMatchObject({ speed: 80 })
47
+ expect(lastFan).toMatchObject({ speed: 80 })
41
48
 
42
49
  // oscillation write
43
50
  const osc = fanCluster.attributes.oscillation
44
51
  await osc.write(true)
45
- expect(last).toMatchObject({ oscillate: true })
52
+ expect(lastFan).toMatchObject({ oscillate: true })
46
53
  })
47
54
 
48
- it('Curtain: reads and writes position', async () => {
49
- const device = new CurtainDevice({ id: 'c-1', type: 'curtain', name: 'C1' }, {})
50
- let last: any = null
55
+ it('curtain: reads and writes position', async () => {
56
+ const device = new CurtainDevice({ id: 'c-1', type: 'curtain', name: 'C1' }, { log: console })
57
+ let lastCurtain: any = null
51
58
  device.getState = async () => ({ position: 42 })
52
- device.setState = async (c: any) => { last = c; return { success: true } }
59
+ device.setState = async (c: any) => {
60
+ lastCurtain = c
61
+ return { success: true }
62
+ }
53
63
 
54
64
  const desc = await device.createMatterAccessory(api)
55
- const shade = desc.clusters.find((c: any) => c.clusterId === 0x0102 || c.type === 'Shade')
65
+ const shade = desc.clusters.find((c: any) => c.clusterId === 0x0102 || c.type === 'WindowCovering')
56
66
  expect(shade).toBeDefined()
57
- const current = shade.attributes.currentPosition || shade.attributes["0x0000"]
67
+ const current = shade.attributes.currentPositionLiftPercent100ths
58
68
  const v = await current.read()
59
- expect(v).toBe(42)
69
+ expect(Math.round(v / 100)).toBe(42)
60
70
 
61
- const target = shade.attributes.targetPosition || shade.attributes["0x0001"]
62
- await target.write(10)
63
- expect(last).toMatchObject({ position: 10 })
71
+ const target = shade.attributes.targetPositionLiftPercent100ths
72
+ await target.write(4200)
73
+ expect(lastCurtain).toMatchObject({ position: 42 })
64
74
  })
65
75
 
66
- it('Lock: reads and writes locked state', async () => {
67
- const device = new LockDevice({ id: 'k-1', type: 'lock', name: 'K1' }, {})
68
- let last: any = null
76
+ it('lock: reads and writes locked state', async () => {
77
+ const device = new LockDevice({ id: 'k-1', type: 'lock', name: 'K1' }, { log: console })
78
+ let lastLock: any = null
69
79
  device.getState = async () => ({ locked: true })
70
- device.setState = async (c: any) => { last = c; return { success: true } }
80
+ device.setState = async (c: any) => {
81
+ lastLock = c
82
+ return { success: true }
83
+ }
71
84
 
72
85
  const desc = await device.createMatterAccessory(api)
73
86
  const lock = desc.clusters.find((c: any) => c.clusterId === 0x0101 || c.type === 'DoorLock')
74
87
  expect(lock).toBeDefined()
75
- const lockAttr = lock.attributes.lockState || lock.attributes["0x0000"]
88
+ const lockAttr = lock.attributes.lockState || lock.attributes['0x0000']
76
89
  const v = await lockAttr.read()
77
90
  expect(v).toBe(true)
78
91
 
79
92
  await lockAttr.write(false)
80
- expect(last).toMatchObject({ locked: false })
93
+ expect(lastLock).toMatchObject({ locked: false })
81
94
  })
82
95
 
83
- it('Humidifier: reads humidity and toggles on/off', async () => {
84
- const device = new HumidifierMatterDevice({ id: 'h-1', type: 'humidifier', name: 'H1' }, {})
85
- let last: any = null
96
+ it('humidifier: reads humidity and toggles on/off', async () => {
97
+ const device = new HumidifierMatterDevice({ id: 'h-1', type: 'humidifier', name: 'H1' }, { log: console })
98
+ let lastHum: any = null
86
99
  device.getState = async () => ({ humidity: 55, on: false })
87
- device.setState = async (c: any) => { last = c; return { success: true } }
100
+ device.setState = async (c: any) => {
101
+ lastHum = c
102
+ return { success: true }
103
+ }
88
104
 
89
105
  const desc = await device.createMatterAccessory(api)
90
106
  const rh = desc.clusters.find((c: any) => c.clusterId === 0x0405 || c.type === 'RelativeHumiditySensor')
91
107
  expect(rh).toBeDefined()
92
- const humAttr = rh.attributes.currentRelativeHumidity || rh.attributes["0x0000"]
108
+ const humAttr = rh.attributes.currentRelativeHumidity || rh.attributes['0x0000']
93
109
  const hv = await humAttr.read()
94
110
  expect(hv).toBe(55)
95
111
 
96
112
  const onCluster = desc.clusters.find((c: any) => c.clusterId === 0x0006 || c.type === 'OnOff')
97
- const onAttr = onCluster.attributes.onOff || onCluster.attributes["0x0000"]
113
+ const onAttr = onCluster.attributes.onOff || onCluster.attributes['0x0000']
98
114
  await onAttr.write(true)
99
- expect(last).toMatchObject({ on: true })
115
+ expect(lastHum).toMatchObject({ on: true })
100
116
  })
101
117
  })
@@ -6,6 +6,37 @@ import { makeFakeApiWithMatter, makeFakeApiWithoutMatter, makeFakeMatterApi } fr
6
6
  // Expanded Matter integration harness tests
7
7
 
8
8
  describe('matter integration harness', () => {
9
+ it('registers blind tilt Matter accessory with correct window covering tilt attributes', async () => {
10
+ const matterApi = makeFakeMatterApi()
11
+ const api = makeFakeApiWithMatter(matterApi)
12
+
13
+ const platform = new SwitchBotMatterPlatform(console as any, {
14
+ devices: [{ id: 'blindtilt-1', type: 'blindtilt', name: 'Blind Tilt 1' }],
15
+ enableMatter: true,
16
+ preferMatter: true,
17
+ log: console,
18
+ } as any, api as any)
19
+
20
+ await (platform as any).registerMatterAccessories()
21
+
22
+ const regs = matterApi.getRegistered()
23
+ expect(regs.length).toBeGreaterThanOrEqual(1)
24
+ const clusters = regs[0]?.clusters
25
+ // Debug: log the actual clusters structure
26
+ // eslint-disable-next-line no-console
27
+ console.log('DEBUG clusters (blindtilt):', JSON.stringify(clusters, null, 2))
28
+ // Support both array and object cluster representations
29
+ let wc = clusters?.windowCovering
30
+ if (!wc && Array.isArray(clusters)) {
31
+ wc = clusters.find((c: any) => c?.type === 'WindowCovering' || c?.clusterId === 0x0102)
32
+ }
33
+ expect(wc).toBeDefined()
34
+ expect(wc).toHaveProperty('currentPositionTiltPercent100ths')
35
+ expect(wc).toHaveProperty('targetPositionTiltPercent100ths')
36
+ expect(wc).toHaveProperty('operationalStatus')
37
+ expect(wc).toHaveProperty('endProductType')
38
+ expect(wc).toHaveProperty('configStatus')
39
+ })
9
40
  it('normalizes Color Bulb and Motion Sensor to Matter-supported categories', async () => {
10
41
  const matterApi = makeFakeMatterApi()
11
42
  const api = makeFakeApiWithMatter(matterApi)
@@ -17,6 +48,7 @@ describe('matter integration harness', () => {
17
48
  ],
18
49
  enableMatter: true,
19
50
  preferMatter: true,
51
+ log: console,
20
52
  } as any, api as any)
21
53
 
22
54
  await (platform as any).registerMatterAccessories()
@@ -28,7 +60,7 @@ describe('matter integration harness', () => {
28
60
  expect(ids).toContain('ms-1')
29
61
  })
30
62
 
31
- it('uses conformant window covering type for curtain Matter registration', async () => {
63
+ it('registers curtain Matter accessory with correct window covering attributes', async () => {
32
64
  const matterApi = makeFakeMatterApi()
33
65
  const api = makeFakeApiWithMatter(matterApi)
34
66
 
@@ -36,14 +68,28 @@ describe('matter integration harness', () => {
36
68
  devices: [{ id: 'curtain-1', type: 'curtain', name: 'Curtain 1' }],
37
69
  enableMatter: true,
38
70
  preferMatter: true,
71
+ log: console,
39
72
  } as any, api as any)
40
73
 
41
74
  await (platform as any).registerMatterAccessories()
42
75
 
43
76
  const regs = matterApi.getRegistered()
44
77
  expect(regs.length).toBeGreaterThanOrEqual(1)
45
- // const wcType = regs[0]?.clusters?.windowCovering?.type
46
- // expect(wcType).toBe(4) // 4 = Curtain (conformant for SwitchBot Curtain)
78
+ const clusters = regs[0]?.clusters
79
+ // Debug: log the actual clusters structure
80
+ // eslint-disable-next-line no-console
81
+ console.log('DEBUG clusters (curtain):', JSON.stringify(clusters, null, 2))
82
+ // Support both array and object cluster representations
83
+ let wc = clusters?.windowCovering
84
+ if (!wc && Array.isArray(clusters)) {
85
+ wc = clusters.find((c: any) => c?.type === 'WindowCovering' || c?.clusterId === 0x0102)
86
+ }
87
+ expect(wc).toBeDefined()
88
+ expect(wc).toHaveProperty('currentPositionLiftPercent100ths')
89
+ expect(wc).toHaveProperty('targetPositionLiftPercent100ths')
90
+ expect(wc).toHaveProperty('operationalStatus')
91
+ expect(wc).toHaveProperty('endProductType')
92
+ expect(wc).toHaveProperty('configStatus')
47
93
  })
48
94
 
49
95
  it('registers RGBIC Neon Wire Rope Light as lightstrip with conformant ColorControl defaults', async () => {
@@ -54,6 +100,7 @@ describe('matter integration harness', () => {
54
100
  devices: [{ id: 'neon-1', type: 'RGBIC Neon Wire Rope Light', name: 'Family Room Neon Wire Rope Light' }],
55
101
  enableMatter: true,
56
102
  preferMatter: true,
103
+ log: console,
57
104
  } as any, api as any)
58
105
 
59
106
  await (platform as any).registerMatterAccessories()
@@ -72,7 +119,7 @@ describe('matter integration harness', () => {
72
119
  const matterApi = makeFakeMatterApi()
73
120
  const api = makeFakeApiWithMatter(matterApi)
74
121
 
75
- const platform = new SwitchBotMatterPlatform(console as any, { devices: [{ id: 'm-1', type: 'light', name: 'L1' }], enableMatter: true, preferMatter: true } as any, api as any)
122
+ const platform = new SwitchBotMatterPlatform(console as any, { devices: [{ id: 'm-1', type: 'light', name: 'L1' }], enableMatter: true, preferMatter: true, log: console } as any, api as any)
76
123
 
77
124
  // Call register path directly
78
125
  if (typeof (platform as any).registerMatterAccessories === 'function') {
@@ -91,7 +138,7 @@ describe('matter integration harness', () => {
91
138
  it('falls back to HAP when Matter API unavailable', async () => {
92
139
  // Directly instantiate the HAP platform to exercise HAP fallback
93
140
  const api = makeFakeApiWithoutMatter()
94
- const platform = new SwitchBotHAPPlatform(console as any, { devices: [{ id: 'h-1', type: 'light', name: 'H1' }], enableMatter: true, preferMatter: true } as any, api as any)
141
+ const platform = new SwitchBotHAPPlatform(console as any, { devices: [{ id: 'h-1', type: 'light', name: 'H1' }], enableMatter: true, preferMatter: true, log: console } as any, api as any)
95
142
 
96
143
  // call loadDevices for HAP flow
97
144
  if (typeof (platform as any).loadDevices === 'function') {
@@ -109,7 +156,7 @@ describe('matter integration harness', () => {
109
156
  const matterApi = makeFakeMatterApi()
110
157
  const api = makeFakeApiWithMatter(matterApi)
111
158
 
112
- const platform = new SwitchBotMatterPlatform(console as any, { devices: [{ id: 'r-1', type: 'light', name: 'R1' }], enableMatter: true, preferMatter: true } as any, api as any)
159
+ const platform = new SwitchBotMatterPlatform(console as any, { devices: [{ id: 'r-1', type: 'light', name: 'R1' }], enableMatter: true, preferMatter: true, log: console } as any, api as any)
113
160
 
114
161
  // Simulate a restored accessory present before registration
115
162
  const restored = { uuid: matterApi.uuid.generate('r-1'), displayName: 'Restored R1', context: { deviceId: 'r-1' } }
@@ -1,4 +1,5 @@
1
- import { describe, it, expect } from 'vitest'
1
+ import { describe, expect, it } from 'vitest'
2
+
2
3
  import { SwitchBotHAPPlatform } from '../src/platform'
3
4
 
4
5
  // Minimal mock logger
@@ -21,7 +22,7 @@ describe('platform integration (HAP)', () => {
21
22
  }
22
23
 
23
24
  // Simple platformAccessory constructor
24
- function PlatformAccessory(name: string, uuid: string) {
25
+ function PlatformAccessory(this: any, name: string, uuid: string) {
25
26
  this.displayName = name
26
27
  this.UUID = uuid
27
28
  this.services = []
@@ -37,10 +38,11 @@ describe('platform integration (HAP)', () => {
37
38
  hap,
38
39
  platformAccessory: PlatformAccessory as any,
39
40
  registerPlatformAccessories: (_p: string, _n: string, accs: any[]) => registered.push(...accs),
40
- on: (_ev: string, cb: Function) => { /* do not auto-run */ },
41
+ on: () => { /* do not auto-run */ },
41
42
  }
42
43
 
43
- const config: any = { devices: [{ id: 'dev1', type: 'light', name: 'L1' }] }
44
+ // Inject logger into config
45
+ const config: any = { devices: [{ id: 'dev1', type: 'light', name: 'L1' }], log }
44
46
 
45
47
  const platform = new SwitchBotHAPPlatform(log as any, config, api)
46
48
 
@@ -49,7 +51,7 @@ describe('platform integration (HAP)', () => {
49
51
 
50
52
  // Expect accessory created and registered
51
53
  expect(registered.length).toBeGreaterThanOrEqual(1)
52
- const keys = Array.from((platform as any).accessories.keys())
54
+ const keys = [...(platform as any).accessories.keys()]
53
55
  expect(keys.some((k: string) => k.includes('dev1'))).toBeTruthy()
54
56
  })
55
57
  })