@switchbot/homebridge-switchbot 5.0.0-beta.133 → 5.0.0-beta.135

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 +67 -19
  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 +38 -30
  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 +69 -21
  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 +38 -29
  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
package/src/platform.ts CHANGED
@@ -97,12 +97,46 @@ export const DEVICE_MATTER_CLUSTERS: Record<string, any> = {
97
97
  }, // Switch in HAP, RobotVacuumCleaner in Matter
98
98
  curtain: {
99
99
  windowCovering: {
100
- type: 5, // GenericWindowCovering - supports lift feature (covers curtain3, rollershade variants via normalization)
100
+ currentPositionLiftPercent100ths: 0,
101
+ targetPositionLiftPercent100ths: 0,
102
+ operationalStatus: {
103
+ global: 0,
104
+ lift: 0,
105
+ tilt: 0,
106
+ },
107
+ endProductType: 0,
108
+ configStatus: {
109
+ operational: true,
110
+ onlineReserved: true,
111
+ liftMovementReversed: false,
112
+ liftPositionAware: true,
113
+ tiltPositionAware: false,
114
+ liftEncoderControlled: true,
115
+ tiltEncoderControlled: false,
116
+ },
101
117
  },
102
118
  }, // WindowCovering → WindowCovering (includes curtain3, rollershade variants via normalization)
103
119
  blindtilt: {
104
120
  windowCovering: {
105
- type: 8, // TiltBlindLiftAndTilt - supports tilt feature
121
+ currentPositionLiftPercent100ths: 0,
122
+ targetPositionLiftPercent100ths: 0,
123
+ currentPositionTiltPercent100ths: 0,
124
+ targetPositionTiltPercent100ths: 0,
125
+ operationalStatus: {
126
+ global: 0,
127
+ lift: 0,
128
+ tilt: 0,
129
+ },
130
+ endProductType: 8,
131
+ configStatus: {
132
+ operational: true,
133
+ onlineReserved: true,
134
+ liftMovementReversed: false,
135
+ liftPositionAware: true,
136
+ tiltPositionAware: true,
137
+ liftEncoderControlled: true,
138
+ tiltEncoderControlled: true,
139
+ },
106
140
  },
107
141
  }, // WindowCovering with tilt → WindowCovering
108
142
  fan: {
@@ -1030,7 +1064,7 @@ export class SwitchBotHAPPlatform {
1030
1064
 
1031
1065
  constructor(log: Logger, config: PlatformConfig, api?: API) {
1032
1066
  this.log = log
1033
- this.config = (config as any) ?? {}
1067
+ this.config = { ...(config as any), logger: log }
1034
1068
  this.api = api
1035
1069
  this.accessories = new Map()
1036
1070
  this.log.info('SwitchBot HAP platform initialized')
@@ -1044,15 +1078,6 @@ export class SwitchBotHAPPlatform {
1044
1078
  this.log.debug('Failed to create shared SwitchBot client', e)
1045
1079
  }
1046
1080
 
1047
- // Create/shared SwitchBot client and attach to config so child devices reuse it.
1048
- try {
1049
- const client = new SwitchBotClient(this.config)
1050
- void client.init()
1051
- ;(this.config as any)._client = client
1052
- } catch (e) {
1053
- this.log.debug('Failed to create shared SwitchBot client', e)
1054
- }
1055
-
1056
1081
  // Wait for Homebridge to finish launching to create/register accessories
1057
1082
  if (this.api && typeof (this.api as any).on === 'function') {
1058
1083
  (this.api as any).on('didFinishLaunching', async () => {
@@ -1133,23 +1158,7 @@ export class SwitchBotHAPPlatform {
1133
1158
  }
1134
1159
 
1135
1160
  const type: string = normalizeTypeForMatter(d.type)
1136
- const windowCoveringTypes = ['curtain', 'curtain3', 'rollershade', 'roller shade', 'blindtilt', 'blind tilt']
1137
- // Patch: If a config override for matterType is present, update DEVICE_MATTER_CLUSTERS for this device
1138
- if (windowCoveringTypes.includes(type.toLowerCase()) && typeof raw.matterType === 'number') {
1139
- if (DEVICE_MATTER_CLUSTERS[type] && DEVICE_MATTER_CLUSTERS[type].windowCovering) {
1140
- DEVICE_MATTER_CLUSTERS[type].windowCovering.type = raw.matterType
1141
- this.log.info(`[Matter/Debug] Overriding DEVICE_MATTER_CLUSTERS.${type}.windowCovering.type to`, raw.matterType)
1142
- }
1143
- }
1144
1161
  const deviceOpts: any = { id: d.id, type, name: d.name, encryptionKey: d.encryptionKey, keyId: d.keyId }
1145
- if (windowCoveringTypes.includes(type.toLowerCase())) {
1146
- if (typeof raw.matterType === 'number') {
1147
- deviceOpts.matterType = raw.matterType
1148
- this.log.info(`[Matter/Debug] Passing matterType override for device ${d.name ?? d.id}: matterType=${raw.matterType}`)
1149
- } else {
1150
- this.log.info(`[Matter/Debug] No matterType override for device ${d.name ?? d.id}`)
1151
- }
1152
- }
1153
1162
  this.log.debug(`[Matter/Debug] Device options for ${d.name ?? d.id}:`, JSON.stringify(deviceOpts, null, 2))
1154
1163
 
1155
1164
  const matterSupported = !!DEVICE_MATTER_SUPPORTED[(type || '').toLowerCase()]
@@ -1353,7 +1362,7 @@ export class SwitchBotMatterPlatform {
1353
1362
 
1354
1363
  constructor(log: Logger, config: PlatformConfig, api?: API) {
1355
1364
  this.log = log
1356
- this.config = (config as any) ?? {}
1365
+ this.config = { ...(config as any), logger: log }
1357
1366
  this.api = api
1358
1367
  this.accessories = new Map()
1359
1368
  this.log.info('SwitchBot Matter platform initialized')
@@ -1,12 +1,13 @@
1
1
  import type { SwitchBotPluginConfig } from './settings.js'
2
- import { SwitchbotOperationError, SwitchbotAuthenticationError, CharacteristicMissingError } from './errors.js'
2
+
3
+ import { CharacteristicMissingError, SwitchbotAuthenticationError, SwitchbotOperationError } from './errors.js'
3
4
 
4
5
  export interface ISwitchBotClient {
5
- init(): Promise<void>
6
- getDevice(id: string): Promise<any>
7
- getDevices(): Promise<any[]>
8
- setDeviceState(id: string, body: any): Promise<any>
9
- destroy(): Promise<void>
6
+ init: () => Promise<void>
7
+ getDevice: (id: string) => Promise<any>
8
+ getDevices: () => Promise<any[]>
9
+ setDeviceState: (id: string, body: any) => Promise<any>
10
+ destroy: () => Promise<void>
10
11
  }
11
12
 
12
13
  /**
@@ -21,15 +22,24 @@ export class SwitchBotClient implements ISwitchBotClient {
21
22
  private requestTimeout = 5000
22
23
  private maxRetries = 2
23
24
  private writeDebounceMs = 100
24
- private logger: any
25
- private pendingWrites: Map<string, { timer: any; body: any; resolvers: Array<{ resolve: (v:any)=>void; reject: (e:any)=>void }>; }> = new Map()
25
+ private logger: import('homebridge').Logger
26
+ private pendingWrites: Map<string, { timer: any, body: any, resolvers: Array<{ resolve: (v: any) => void, reject: (e: any) => void }> }> = new Map()
26
27
 
27
28
  constructor(cfg: SwitchBotPluginConfig) {
28
29
  this.cfg = cfg
29
- this.logger = (cfg as any)?.logger ?? console
30
- if (typeof (cfg as any)?.requestTimeout === 'number') this.requestTimeout = (cfg as any).requestTimeout
31
- if (typeof (cfg as any)?.maxRetries === 'number') this.maxRetries = (cfg as any).maxRetries
32
- if (typeof (cfg as any)?.writeDebounceMs === 'number') this.writeDebounceMs = (cfg as any).writeDebounceMs
30
+ this.logger = (cfg as any)?.logger as import('homebridge').Logger
31
+ if (!this.logger) {
32
+ throw new Error('SwitchBotClient requires a logger (Homebridge logger) in config')
33
+ }
34
+ if (typeof (cfg as any)?.requestTimeout === 'number') {
35
+ this.requestTimeout = (cfg as any).requestTimeout
36
+ }
37
+ if (typeof (cfg as any)?.maxRetries === 'number') {
38
+ this.maxRetries = (cfg as any).maxRetries
39
+ }
40
+ if (typeof (cfg as any)?.writeDebounceMs === 'number') {
41
+ this.writeDebounceMs = (cfg as any).writeDebounceMs
42
+ }
33
43
  }
34
44
 
35
45
  async init(): Promise<void> {
@@ -40,12 +50,12 @@ export class SwitchBotClient implements ISwitchBotClient {
40
50
  token: this.cfg.openApiToken,
41
51
  secret: this.cfg.openApiSecret,
42
52
  // Enable all built-in resilience features from node-switchbot v4
43
- enableFallback: true, // Auto-fallback from BLE to API
44
- enableRetry: true, // Retry with exponential backoff
45
- enableCircuitBreaker: true, // Circuit breaker per connection type
46
- enableMetrics: true, // Connection tracking and statistics
47
- enableBLE: true, // Enable BLE scanning by default
48
- scanDuration: 5000, // BLE scan duration in milliseconds
53
+ enableFallback: true, // Auto-fallback from BLE to API
54
+ enableRetry: true, // Retry with exponential backoff
55
+ enableCircuitBreaker: true, // Circuit breaker per connection type
56
+ enableMetrics: true, // Connection tracking and statistics
57
+ enableBLE: true, // Enable BLE scanning by default
58
+ scanDuration: 5000, // BLE scan duration in milliseconds
49
59
  ...(typeof (this.cfg as any)?.nodeClientConfig === 'object' && (this.cfg as any).nodeClientConfig),
50
60
  })
51
61
  this.logger?.info?.('SwitchBot client initialized with native resilience features')
@@ -65,7 +75,7 @@ export class SwitchBotClient implements ISwitchBotClient {
65
75
  const controller = new AbortController()
66
76
  const timer = setTimeout(() => controller.abort(), to)
67
77
  try {
68
- const fetchOpts = Object.assign({}, opts)
78
+ const fetchOpts = { ...opts }
69
79
  try {
70
80
  Object.defineProperty(fetchOpts, 'signal', { value: controller.signal, enumerable: false, configurable: true })
71
81
  } catch (_e) {
@@ -73,7 +83,9 @@ export class SwitchBotClient implements ISwitchBotClient {
73
83
  }
74
84
  const resp = await fetch(url, fetchOpts)
75
85
  clearTimeout(timer)
76
- if (resp.ok) return resp
86
+ if (resp.ok) {
87
+ return resp
88
+ }
77
89
  // server error / rate limit: retry
78
90
  if (resp.status >= 500 || resp.status === 429) {
79
91
  throw new Error(`retriable response: ${resp.status}`)
@@ -81,10 +93,12 @@ export class SwitchBotClient implements ISwitchBotClient {
81
93
  return resp
82
94
  } catch (err: any) {
83
95
  clearTimeout(timer)
84
- if (attempt > max) throw err
96
+ if (attempt > max) {
97
+ throw err
98
+ }
85
99
  // exponential backoff
86
- const backoff = Math.min(100 * Math.pow(2, attempt - 1), 2000)
87
- await new Promise((r) => setTimeout(r, backoff))
100
+ const backoff = Math.min(100 * 2 ** (attempt - 1), 2000)
101
+ await new Promise(r => setTimeout(r, backoff))
88
102
  }
89
103
  }
90
104
  }
@@ -151,10 +165,12 @@ export class SwitchBotClient implements ISwitchBotClient {
151
165
  return
152
166
  }
153
167
 
154
- const resolvers: Array<{ resolve: (v:any)=>void; reject: (e:any)=>void }> = [{ resolve, reject }]
168
+ const resolvers: Array<{ resolve: (v: any) => void, reject: (e: any) => void }> = [{ resolve, reject }]
155
169
  const timer = setTimeout(async () => {
156
170
  const entry = this.pendingWrites.get(id)
157
- if (!entry) return
171
+ if (!entry) {
172
+ return
173
+ }
158
174
  this.pendingWrites.delete(id)
159
175
  try {
160
176
  const out = await this._doSetDeviceState(id, entry.body)
@@ -224,7 +240,7 @@ export class SwitchBotClient implements ISwitchBotClient {
224
240
  const opts = {
225
241
  method: 'POST',
226
242
  headers: {
227
- Authorization: this.cfg.openApiToken,
243
+ 'Authorization': this.cfg.openApiToken,
228
244
  'Content-Type': 'application/json',
229
245
  },
230
246
  body: JSON.stringify(body),
@@ -248,4 +264,3 @@ export class SwitchBotClient implements ISwitchBotClient {
248
264
  this.client = null
249
265
  }
250
266
  }
251
-
@@ -1,4 +1,5 @@
1
- import { describe, it, expect } from 'vitest'
1
+ import { describe, expect, it } from 'vitest'
2
+
2
3
  import { SwitchBotHAPPlatform, SwitchBotMatterPlatform } from '../src/platform'
3
4
 
4
5
  const log = { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }
@@ -8,7 +9,7 @@ describe('accessory restore', () => {
8
9
  const registered: any[] = []
9
10
  const hap = { uuid: { generate: (s: string) => `uuid-${s}` }, Service: {}, Characteristic: {} }
10
11
 
11
- function PlatformAccessory(name: string, uuid: string) {
12
+ function PlatformAccessory(this: any, name: string, uuid: string) {
12
13
  this.displayName = name
13
14
  this.UUID = uuid
14
15
  this.context = {}
@@ -20,10 +21,10 @@ describe('accessory restore', () => {
20
21
  hap,
21
22
  platformAccessory: PlatformAccessory as any,
22
23
  registerPlatformAccessories: (_p: string, _n: string, accs: any[]) => registered.push(...accs),
23
- on: (_ev: string, cb: Function) => {},
24
+ on: () => {},
24
25
  }
25
26
 
26
- const platform = new SwitchBotHAPPlatform(log as any, { devices: [{ id: 'dev1', type: 'light', name: 'NewName' }] }, api)
27
+ const platform = new SwitchBotHAPPlatform(log as any, { platform: 'SwitchBot', devices: [{ id: 'dev1', type: 'light', name: 'NewName' }] }, api)
27
28
 
28
29
  // Simulate restored accessory with different uuid but correct deviceId in context
29
30
  const restored = new (api.platformAccessory)('OldName', 'old-uuid')
@@ -36,7 +37,7 @@ describe('accessory restore', () => {
36
37
  // No new registration for this device because restored accessory was reused
37
38
  expect(registered.length).toBe(0)
38
39
  // The restored accessory should remain in the platform's accessory map
39
- const keys = Array.from((platform as any).accessories.keys())
40
+ const keys = [...(platform as any).accessories.keys()]
40
41
  expect(keys).toContain('old-uuid')
41
42
  const acc = (platform as any).accessories.get('old-uuid')
42
43
  expect(acc.context.deviceId).toBe('dev1')
@@ -49,10 +50,10 @@ describe('accessory restore', () => {
49
50
  matter,
50
51
  isMatterAvailable: () => true,
51
52
  isMatterEnabled: () => true,
52
- on: (_ev: string, cb: Function) => {},
53
+ on: () => {},
53
54
  }
54
55
 
55
- const platform = new SwitchBotMatterPlatform(log as any, { devices: [{ id: 'm1', type: 'light', name: 'M1' }] }, api)
56
+ const platform = new SwitchBotMatterPlatform(log as any, { platform: 'SwitchBot', devices: [{ id: 'm1', type: 'light', name: 'M1' }] }, api)
56
57
 
57
58
  // Simulate restored Matter accessory with a different uuid but matching deviceId
58
59
  const restored = { uuid: 'restored-uuid', displayName: 'OldM', context: { deviceId: 'm1', type: 'light' } }
@@ -65,7 +66,7 @@ describe('accessory restore', () => {
65
66
  await (platform as any).registerMatterAccessories()
66
67
 
67
68
  // The restored accessory should still be present and associated with deviceId
68
- const keys = Array.from((platform as any).accessories.keys())
69
+ const keys = [...(platform as any).accessories.keys()]
69
70
  expect(keys).toContain(restored.uuid)
70
71
  const acc = (platform as any).accessories.get(restored.uuid)
71
72
  expect(acc.context.deviceId).toBe('m1')
@@ -1,4 +1,5 @@
1
- import { describe, it, expect } from 'vitest'
1
+ import { describe, expect, it } from 'vitest'
2
+
2
3
  import { createDevice } from '../src/deviceFactory'
3
4
 
4
5
  const DEVICE_TYPES = [
@@ -18,7 +19,7 @@ const DEVICE_TYPES = [
18
19
  describe('device mapping', () => {
19
20
  it('maps known device types to device descriptors (HAP)', async () => {
20
21
  for (const t of DEVICE_TYPES) {
21
- const created = await createDevice({ id: `dev-${t}`, type: t, name: `Name-${t}` }, {}, false)
22
+ const created = await createDevice({ id: `dev-${t}`, type: t, name: `Name-${t}` }, { log: console, logger: console }, false)
22
23
  expect(created).toHaveProperty('instance')
23
24
  expect(created).toHaveProperty('createAccessory')
24
25
  const acc = created.createAccessory?.(undefined)
@@ -31,7 +32,7 @@ describe('device mapping', () => {
31
32
  })
32
33
 
33
34
  it('returns matter protocol when requested', async () => {
34
- const created = await createDevice({ id: 'm1', type: 'light', name: 'M1' }, { preferMatter: true, enableMatter: true }, true)
35
+ const created = await createDevice({ id: 'm1', type: 'light', name: 'M1' }, { preferMatter: true, enableMatter: true, log: console, logger: console }, true)
35
36
  expect(created.protocol).toBe('matter')
36
37
  })
37
38
  })
@@ -1,9 +1,10 @@
1
- import { describe, it, expect } from 'vitest'
1
+ import { describe, expect, it } from 'vitest'
2
+
2
3
  import { createDevice } from '../src/deviceFactory'
3
4
 
4
5
  describe('deviceFactory', () => {
5
6
  it('creates HAP device descriptor for light', async () => {
6
- const created = await createDevice({ id: 'dev1', type: 'light', name: 'L1' }, {}, false)
7
+ const created = await createDevice({ id: 'dev1', type: 'light', name: 'L1' }, { log: console, logger: console }, false)
7
8
  expect(created).toHaveProperty('instance')
8
9
  expect(created).toHaveProperty('createAccessory')
9
10
  expect(created.protocol).toBe('hap')
@@ -12,7 +13,7 @@ describe('deviceFactory', () => {
12
13
  })
13
14
 
14
15
  it('creates Matter descriptor when useMatter=true', async () => {
15
- const created = await createDevice({ id: 'dev2', type: 'light', name: 'L2' }, { preferMatter: true, enableMatter: true }, true)
16
+ const created = await createDevice({ id: 'dev2', type: 'light', name: 'L2' }, { preferMatter: true, enableMatter: true, log: console, logger: console }, true)
16
17
  expect(created.protocol).toBe('matter')
17
18
  })
18
19
  })
@@ -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' })