@switchbot/homebridge-switchbot 5.0.0-beta.154 → 5.0.0-beta.155

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 (67) hide show
  1. package/.github/workflows/release.yml +63 -15
  2. package/.github/workflows/stale.yml +2 -4
  3. package/CHANGELOG.md +21 -29
  4. package/MIGRATION.md +6 -6
  5. package/README.md +5 -3
  6. package/dist/device-types.js +7 -7
  7. package/dist/device-types.js.map +1 -1
  8. package/dist/deviceFactory.d.ts +1 -1
  9. package/dist/deviceFactory.d.ts.map +1 -1
  10. package/dist/deviceFactory.js +20 -20
  11. package/dist/deviceFactory.js.map +1 -1
  12. package/dist/homebridge-ui/device-types.js +246 -0
  13. package/dist/homebridge-ui/device-types.js.map +1 -0
  14. package/dist/homebridge-ui/deviceCommandMapper.js +319 -0
  15. package/dist/homebridge-ui/deviceCommandMapper.js.map +1 -0
  16. package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -1
  17. package/dist/homebridge-ui/endpoints/discovery.js +5 -1
  18. package/dist/homebridge-ui/endpoints/discovery.js.map +1 -1
  19. package/dist/homebridge-ui/errors.js +32 -0
  20. package/dist/homebridge-ui/errors.js.map +1 -0
  21. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js +90 -0
  22. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js.map +1 -0
  23. package/dist/homebridge-ui/homebridge-ui/endpoints/devices.js +144 -0
  24. package/dist/homebridge-ui/homebridge-ui/endpoints/devices.js.map +1 -0
  25. package/dist/homebridge-ui/homebridge-ui/endpoints/discovery.js +219 -0
  26. package/dist/homebridge-ui/homebridge-ui/endpoints/discovery.js.map +1 -0
  27. package/dist/homebridge-ui/homebridge-ui/server.js +11 -0
  28. package/dist/homebridge-ui/homebridge-ui/server.js.map +1 -0
  29. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js +108 -0
  30. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js.map +1 -0
  31. package/dist/homebridge-ui/homebridge-ui/utils/device-migration.js +111 -0
  32. package/dist/homebridge-ui/homebridge-ui/utils/device-migration.js.map +1 -0
  33. package/dist/homebridge-ui/homebridge-ui/utils/logger.js +17 -0
  34. package/dist/homebridge-ui/homebridge-ui/utils/logger.js.map +1 -0
  35. package/dist/homebridge-ui/public/js/app.js +5 -9
  36. package/dist/homebridge-ui/public/js/app.js.map +2 -2
  37. package/dist/homebridge-ui/settings.js +8 -0
  38. package/dist/homebridge-ui/settings.js.map +1 -0
  39. package/dist/homebridge-ui/switchbotClient.js +247 -0
  40. package/dist/homebridge-ui/switchbotClient.js.map +1 -0
  41. package/dist/switchbotClient.d.ts +7 -1
  42. package/dist/switchbotClient.d.ts.map +1 -1
  43. package/dist/switchbotClient.js +82 -10
  44. package/dist/switchbotClient.js.map +1 -1
  45. package/docs/assets/main.js +1 -1
  46. package/docs/index.html +10 -4
  47. package/docs/variables/default.html +1 -1
  48. package/eslint.config.js +9 -10
  49. package/package.json +25 -23
  50. package/src/device-types.js +246 -0
  51. package/src/device-types.js.map +1 -0
  52. package/src/device-types.ts +7 -7
  53. package/src/deviceCommandMapper.js +319 -0
  54. package/src/deviceCommandMapper.js.map +1 -0
  55. package/src/deviceFactory.ts +22 -21
  56. package/src/errors.js +32 -0
  57. package/src/errors.js.map +1 -0
  58. package/src/homebridge-ui/endpoints/discovery.ts +5 -1
  59. package/src/settings.js +8 -0
  60. package/src/settings.js.map +1 -0
  61. package/src/switchbotClient.js +247 -0
  62. package/src/switchbotClient.js.map +1 -0
  63. package/src/switchbotClient.ts +95 -10
  64. package/test/client/switchbotClient.spec.ts +42 -1
  65. package/test/e2e/run-e2e.spec.ts +1 -0
  66. package/tsconfig.ui.json +11 -0
  67. package/.github/workflows/beta-release.yml +0 -52
@@ -1,6 +1,7 @@
1
- import type { DeviceType, SwitchBotPluginConfig } from './settings.js'
2
1
  import type { Logger } from 'homebridge'
3
2
 
3
+ import type { DeviceType, SwitchBotPluginConfig } from './settings.js'
4
+
4
5
  import { DEVICE_TYPE_NORMALIZATION_MAP } from './device-types.js'
5
6
  import {
6
7
  BlindTiltDevice,
@@ -51,12 +52,12 @@ const DEVICE_CLASS_MAP: Record<string, any> = {
51
52
  'lightstrip': LightStripDevice,
52
53
  'motion': MotionSensorDevice,
53
54
  'contact': ContactSensorDevice,
54
- 'vacuum': VacuumDevice, // node-switchbot beta
55
+ 'vacuum': VacuumDevice,
55
56
  // Canonical, normalized device type keys (lowercase, mapped to device classes)
56
57
  'video doorbell': GenericDevice,
57
- 'smart radiator thermostat': GenericDevice, // node-switchbot beta
58
+ 'smart radiator thermostat': GenericDevice,
58
59
  'woiosensor': GenericDevice,
59
- 'garage door opener': GenericDevice, // node-switchbot beta
60
+ 'garage door opener': GenericDevice,
60
61
  'air purifier table pm2.5': GenericDevice,
61
62
  'air purifier voc': GenericDevice,
62
63
  'air purifier table voc': GenericDevice,
@@ -65,9 +66,9 @@ const DEVICE_CLASS_MAP: Record<string, any> = {
65
66
  'meterpro(co2)': MeterDevice,
66
67
  'walletfinder': WalletFinderDevice,
67
68
  'plug': PlugDevice,
68
- 'plug mini (eu)': PlugMiniDevice, // node-switchbot beta
69
- 'plug mini (jp)': PlugMiniDevice, // node-switchbot beta
70
- 'plug mini (us)': PlugMiniDevice, // node-switchbot beta
69
+ 'plug mini (eu)': PlugMiniDevice,
70
+ 'plug mini (jp)': PlugMiniDevice,
71
+ 'plug mini (us)': PlugMiniDevice,
71
72
  'relay switch 1pm': RelaySwitch1PMDevice,
72
73
  'relay switch 2pm': RelaySwitch1PMDevice,
73
74
  'k10+ pro': WoSweeperDevice,
@@ -77,26 +78,26 @@ const DEVICE_CLASS_MAP: Record<string, any> = {
77
78
  'ai hub': GenericDevice,
78
79
  'hub': GenericDevice,
79
80
  'hub 2': Hub2Device,
80
- 'hub 3': GenericDevice, // node-switchbot beta
81
- 'hub mini': GenericDevice, // node-switchbot beta
81
+ 'hub 3': GenericDevice,
82
+ 'hub mini': GenericDevice,
82
83
  'hub plus': GenericDevice,
83
84
  'indoor cam': GenericDevice,
84
85
  'pan/tilt cam': GenericDevice,
85
86
  'pan/tilt cam 2k': GenericDevice,
86
87
  'pan/tilt cam plus 2k': GenericDevice,
87
88
  'pan/tilt cam plus 3k': GenericDevice,
88
- 'humidifier': HumidifierDevice, // node-switchbot beta (Evaporative Humidifier)
89
- 'roller shade': RollerShadeDevice, // node-switchbot beta
90
- 'strip light 3': StripLightDevice, // node-switchbot beta
91
- 'circulator fan': FanDevice, // node-switchbot beta
92
- 'smart lock pro': LockDevice, // node-switchbot beta
93
- 'lock lite': LockDevice, // node-switchbot beta
94
- 'keypad': LockDevice, // node-switchbot beta
95
- 'lock vision pro': LockDevice, // node-switchbot beta
96
- 'floor lamp': LightDevice, // node-switchbot beta
97
- 'rgbicww floor lamp': LightStripDevice, // node-switchbot beta
98
- 'rgbicww strip light': LightStripDevice, // node-switchbot beta
99
- 'home climate panel': GenericDevice, // node-switchbot beta (Climate Panel)
89
+ 'humidifier': HumidifierDevice, // Includes evaporative humidifier mapping
90
+ 'roller shade': RollerShadeDevice,
91
+ 'strip light 3': StripLightDevice,
92
+ 'circulator fan': FanDevice,
93
+ 'smart lock pro': LockDevice,
94
+ 'lock lite': LockDevice,
95
+ 'keypad': LockDevice,
96
+ 'lock vision pro': LockDevice,
97
+ 'floor lamp': LightDevice,
98
+ 'rgbicww floor lamp': LightStripDevice,
99
+ 'rgbicww strip light': LightStripDevice,
100
+ 'home climate panel': GenericDevice, // Climate panel family
100
101
  'lock': LockDevice,
101
102
  'humidifier2': HumidifierDevice,
102
103
  'temperature': TemperatureSensorDevice,
package/src/errors.js ADDED
@@ -0,0 +1,32 @@
1
+ // Error classes for node-switchbot v4 compatibility
2
+ // Always use local fallback classes for compatibility; upstream error classes are not guaranteed to exist
3
+ const SwitchbotOperationError = class extends Error {
4
+ code;
5
+ cause;
6
+ constructor(message, code, cause) {
7
+ super(message);
8
+ this.name = 'SwitchbotOperationError';
9
+ this.code = code;
10
+ this.cause = cause;
11
+ }
12
+ };
13
+ const SwitchbotAuthenticationError = class extends Error {
14
+ code;
15
+ cause;
16
+ constructor(message, code, cause) {
17
+ super(message);
18
+ this.name = 'SwitchbotAuthenticationError';
19
+ this.code = code;
20
+ this.cause = cause;
21
+ }
22
+ };
23
+ const CharacteristicMissingError = class extends Error {
24
+ characteristic;
25
+ constructor(message, characteristic) {
26
+ super(message);
27
+ this.name = 'CharacteristicMissingError';
28
+ this.characteristic = characteristic;
29
+ }
30
+ };
31
+ export { CharacteristicMissingError, SwitchbotAuthenticationError, SwitchbotOperationError };
32
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["errors.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,0GAA0G;AAE1G,MAAM,uBAAuB,GAAG,KAAM,SAAQ,KAAK;IACjD,IAAI,CAAS;IACb,KAAK,CAAQ;IACb,YAAY,OAAe,EAAE,IAAa,EAAE,KAAa;QACvD,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAA;QACrC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;CACF,CAAA;AAED,MAAM,4BAA4B,GAAG,KAAM,SAAQ,KAAK;IACtD,IAAI,CAAS;IACb,KAAK,CAAQ;IACb,YAAY,OAAe,EAAE,IAAa,EAAE,KAAa;QACvD,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,8BAA8B,CAAA;QAC1C,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;CACF,CAAA;AAED,MAAM,0BAA0B,GAAG,KAAM,SAAQ,KAAK;IACpD,cAAc,CAAQ;IACtB,YAAY,OAAe,EAAE,cAAsB;QACjD,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,4BAA4B,CAAA;QACxC,IAAI,CAAC,cAAc,GAAG,cAAc,CAAA;IACtC,CAAC;CACF,CAAA;AAED,OAAO,EAAE,0BAA0B,EAAE,4BAA4B,EAAE,uBAAuB,EAAE,CAAA"}
@@ -22,6 +22,8 @@ export function registerDiscoveryEndpoint(server: HomebridgePluginUiServer) {
22
22
  enableBLE: true,
23
23
  enableFallback: true,
24
24
  enableRetry: true,
25
+ enableCircuitBreaker: true,
26
+ enableConnectionIntelligence: true,
25
27
  })
26
28
 
27
29
  await switchbot.discover({ timeout: 1000 })
@@ -50,7 +52,7 @@ export function registerDiscoveryEndpoint(server: HomebridgePluginUiServer) {
50
52
  const token = getCredential(platform, 'openApiToken') || platform.token
51
53
  const secret = getCredential(platform, 'openApiSecret') || platform.secret
52
54
 
53
- const hasOpenAPICredentials = !!token
55
+ const hasOpenAPICredentials = !!(token && secret)
54
56
 
55
57
  if (!hasOpenAPICredentials) {
56
58
  uiLog.warn('GET /discover - No OpenAPI credentials found, will attempt BLE-only discovery')
@@ -66,6 +68,8 @@ export function registerDiscoveryEndpoint(server: HomebridgePluginUiServer) {
66
68
  enableBLE: true,
67
69
  enableFallback: true,
68
70
  enableRetry: true,
71
+ enableCircuitBreaker: true,
72
+ enableConnectionIntelligence: true,
69
73
  })
70
74
 
71
75
  const deviceMap = new Map<string, any>()
@@ -0,0 +1,8 @@
1
+ export const PLUGIN_NAME = '@switchbot/homebridge-switchbot';
2
+ export const PLATFORM_NAME = 'SwitchBot';
3
+ export const DEFAULT_CONFIG = {
4
+ preferMatter: true,
5
+ enableMatter: true,
6
+ enableBLE: true,
7
+ };
8
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["settings.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,iCAAiC,CAAA;AAC5D,MAAM,CAAC,MAAM,aAAa,GAAG,WAAW,CAAA;AAYxC,MAAM,CAAC,MAAM,cAAc,GAAmC;IAC5D,YAAY,EAAE,IAAI;IAClB,YAAY,EAAE,IAAI;IAClB,SAAS,EAAE,IAAI;CAChB,CAAA"}
@@ -0,0 +1,247 @@
1
+ import { getDeviceCommandHandler } from './deviceCommandMapper.js';
2
+ import { CharacteristicMissingError, SwitchbotAuthenticationError, SwitchbotOperationError } from './errors.js';
3
+ /**
4
+ * Thin wrapper around node-switchbot v4.0.0+
5
+ * Leverages upstream resilience features (retry, circuit breaker, connection intelligence)
6
+ * while maintaining plugin-specific features like write debouncing and OpenAPI fallback.
7
+ */
8
+ export class SwitchBotClient {
9
+ cfg;
10
+ client = null;
11
+ writeDebounceMs = 100;
12
+ discoveryCacheTtlMs = 30_000;
13
+ lastDiscoveryAt = 0;
14
+ logger;
15
+ pendingWrites = new Map();
16
+ constructor(cfg) {
17
+ this.cfg = cfg;
18
+ this.logger = cfg?.logger;
19
+ if (!this.logger) {
20
+ throw new Error('SwitchBotClient requires a logger (Homebridge logger) in config');
21
+ }
22
+ if (typeof cfg?.writeDebounceMs === 'number') {
23
+ this.writeDebounceMs = cfg.writeDebounceMs;
24
+ }
25
+ if (typeof cfg?.discoveryCacheTtlMs === 'number') {
26
+ this.discoveryCacheTtlMs = Math.max(0, cfg.discoveryCacheTtlMs);
27
+ }
28
+ }
29
+ async init() {
30
+ if (this.client) {
31
+ return;
32
+ }
33
+ try {
34
+ // Dynamic import of node-switchbot v4 with native resilience features
35
+ const { SwitchBot } = await import('node-switchbot');
36
+ const rawNodeClientConfig = typeof this.cfg?.nodeClientConfig === 'object' ? this.cfg.nodeClientConfig : {};
37
+ const scanTimeout = this.resolveScanTimeoutMs(rawNodeClientConfig);
38
+ this.client = new SwitchBot({
39
+ token: this.cfg.openApiToken,
40
+ secret: this.cfg.openApiSecret,
41
+ // Enable built-in resilience features from node-switchbot v4.
42
+ enableFallback: true, // Auto-fallback from BLE to API
43
+ enableRetry: true, // Retry with exponential backoff
44
+ enableCircuitBreaker: true, // Circuit breaker per connection type
45
+ enableConnectionIntelligence: true, // Connection tracking and route preference
46
+ enableBLE: this.cfg.enableBLE !== false, // Use config value, default true
47
+ scanTimeout,
48
+ ...rawNodeClientConfig,
49
+ });
50
+ this.lastDiscoveryAt = 0;
51
+ this.logger?.info?.('SwitchBot client initialized with native resilience features');
52
+ }
53
+ catch (e) {
54
+ this.logger?.warn?.('Failed to load node-switchbot; will use OpenAPI fallback:', e);
55
+ this.client = null;
56
+ }
57
+ }
58
+ async getDevice(id) {
59
+ if (this.client) {
60
+ try {
61
+ const fromManager = this.getManagedDevice(id);
62
+ if (fromManager) {
63
+ return fromManager;
64
+ }
65
+ const devices = await this.ensureDiscovered(false);
66
+ const fromDiscovery = devices.find((d) => d.id === id);
67
+ if (fromDiscovery) {
68
+ return fromDiscovery;
69
+ }
70
+ const refreshDevices = await this.ensureDiscovered(true);
71
+ return refreshDevices.find((d) => d.id === id);
72
+ }
73
+ catch (e) {
74
+ if (e instanceof SwitchbotAuthenticationError) {
75
+ this.logger?.error?.(`Authentication error for getDevice(${id}):`, e.message);
76
+ throw e;
77
+ }
78
+ else if (e instanceof SwitchbotOperationError) {
79
+ this.logger?.warn?.(`Operation error for getDevice(${id}):`, e.message, e.code);
80
+ throw e;
81
+ }
82
+ else if (e instanceof CharacteristicMissingError) {
83
+ this.logger?.warn?.(`Characteristic missing for getDevice(${id}):`, e.characteristic);
84
+ throw e;
85
+ }
86
+ else {
87
+ this.logger?.warn?.(`Client getDevice failed for ${id}:`, e);
88
+ throw e;
89
+ }
90
+ }
91
+ }
92
+ throw new SwitchbotOperationError('No SwitchBot client available', 'no_client');
93
+ }
94
+ async getDevices() {
95
+ if (this.client) {
96
+ try {
97
+ const fromManager = this.getManagedDevices();
98
+ if (fromManager.length > 0) {
99
+ return fromManager;
100
+ }
101
+ return await this.ensureDiscovered(false);
102
+ }
103
+ catch (e) {
104
+ this.logger?.warn?.('Client getDevices failed:', e);
105
+ throw e;
106
+ }
107
+ }
108
+ throw new SwitchbotOperationError('No SwitchBot client available', 'no_client');
109
+ }
110
+ async setDeviceState(id, body) {
111
+ // Plugin-level debounce: coalesce rapid writes per device
112
+ if (!this.writeDebounceMs || this.writeDebounceMs <= 0) {
113
+ return this._doSetDeviceState(id, body);
114
+ }
115
+ return new Promise((resolve, reject) => {
116
+ const existing = this.pendingWrites.get(id);
117
+ if (existing) {
118
+ existing.body = body;
119
+ existing.resolvers.push({ resolve, reject });
120
+ return;
121
+ }
122
+ const resolvers = [{ resolve, reject }];
123
+ const timer = setTimeout(async () => {
124
+ const entry = this.pendingWrites.get(id);
125
+ if (!entry) {
126
+ return;
127
+ }
128
+ this.pendingWrites.delete(id);
129
+ try {
130
+ const out = await this._doSetDeviceState(id, entry.body);
131
+ for (const r of entry.resolvers)
132
+ r.resolve(out);
133
+ }
134
+ catch (e) {
135
+ if (e instanceof SwitchbotAuthenticationError) {
136
+ this.logger?.error?.(`Authentication error for setDeviceState(${id}):`, e.message);
137
+ }
138
+ else if (e instanceof SwitchbotOperationError) {
139
+ this.logger?.warn?.(`Operation error for setDeviceState(${id}):`, e.message, e.code);
140
+ }
141
+ else if (e instanceof CharacteristicMissingError) {
142
+ this.logger?.warn?.(`Characteristic missing for setDeviceState(${id}):`, e.characteristic);
143
+ }
144
+ for (const r of entry.resolvers)
145
+ r.reject(e);
146
+ }
147
+ }, this.writeDebounceMs);
148
+ this.pendingWrites.set(id, { timer, body, resolvers });
149
+ });
150
+ }
151
+ async _doSetDeviceState(id, body) {
152
+ if (!this.client) {
153
+ throw new SwitchbotOperationError('No SwitchBot client available for setDeviceState', 'no_client');
154
+ }
155
+ try {
156
+ const device = await this.getDevice(id);
157
+ if (!device) {
158
+ throw new SwitchbotOperationError(`Device ${id} not found`, 'device_not_found');
159
+ }
160
+ const deviceType = (device.deviceType ?? '').toLowerCase();
161
+ const command = body?.command;
162
+ if (!command) {
163
+ throw new SwitchbotOperationError('No command specified in body', 'no_command');
164
+ }
165
+ const handler = getDeviceCommandHandler(deviceType, command);
166
+ if (!handler) {
167
+ throw new SwitchbotOperationError(`Unsupported command '${command}' for device type '${deviceType}'`, 'unsupported_command');
168
+ }
169
+ this.logger?.debug?.(`[${id}] Calling mapped command '${command}' for device type '${deviceType}'`);
170
+ return await handler(device, body);
171
+ }
172
+ catch (e) {
173
+ if (e instanceof SwitchbotAuthenticationError) {
174
+ this.logger?.error?.(`Authentication error for setDeviceState(${id}):`, e.message);
175
+ throw e;
176
+ }
177
+ else if (e instanceof SwitchbotOperationError) {
178
+ this.logger?.warn?.(`Operation error for setDeviceState(${id}):`, e.message, e.code);
179
+ throw e;
180
+ }
181
+ else if (e instanceof CharacteristicMissingError) {
182
+ this.logger?.warn?.(`Characteristic missing for setDeviceState(${id}):`, e.characteristic);
183
+ throw e;
184
+ }
185
+ else {
186
+ this.logger?.warn?.(`Device command failed for ${id}:`, e);
187
+ throw e;
188
+ }
189
+ }
190
+ }
191
+ async destroy() {
192
+ for (const [, pending] of this.pendingWrites) {
193
+ clearTimeout(pending.timer);
194
+ const err = new SwitchbotOperationError('Client destroyed before pending write was sent', 'client_destroyed');
195
+ for (const r of pending.resolvers) {
196
+ r.reject(err);
197
+ }
198
+ }
199
+ this.pendingWrites.clear();
200
+ if (this.client?.cleanup) {
201
+ await this.client.cleanup();
202
+ }
203
+ this.client = null;
204
+ this.lastDiscoveryAt = 0;
205
+ }
206
+ resolveScanTimeoutMs(rawNodeClientConfig) {
207
+ if (typeof rawNodeClientConfig.scanTimeout === 'number' && Number.isFinite(rawNodeClientConfig.scanTimeout)) {
208
+ return Math.max(500, rawNodeClientConfig.scanTimeout);
209
+ }
210
+ if (typeof rawNodeClientConfig.scanDuration === 'number' && Number.isFinite(rawNodeClientConfig.scanDuration)) {
211
+ return Math.max(500, rawNodeClientConfig.scanDuration);
212
+ }
213
+ if (typeof this.cfg?.bleScanDurationSeconds === 'number') {
214
+ return Math.max(500, this.cfg.bleScanDurationSeconds * 1000);
215
+ }
216
+ return 5000;
217
+ }
218
+ getManagedDevice(id) {
219
+ const manager = this.client?.devices;
220
+ if (manager?.get) {
221
+ return manager.get(id);
222
+ }
223
+ return undefined;
224
+ }
225
+ getManagedDevices() {
226
+ const manager = this.client?.devices;
227
+ if (manager?.list) {
228
+ const list = manager.list();
229
+ return Array.isArray(list) ? list : [];
230
+ }
231
+ return [];
232
+ }
233
+ async ensureDiscovered(force) {
234
+ if (!this.client) {
235
+ throw new SwitchbotOperationError('No SwitchBot client available', 'no_client');
236
+ }
237
+ const fromManager = this.getManagedDevices();
238
+ const cacheValid = this.discoveryCacheTtlMs > 0 && (Date.now() - this.lastDiscoveryAt) < this.discoveryCacheTtlMs;
239
+ if (!force && cacheValid && fromManager.length > 0) {
240
+ return fromManager;
241
+ }
242
+ const discovered = await this.client.discover();
243
+ this.lastDiscoveryAt = Date.now();
244
+ return discovered;
245
+ }
246
+ }
247
+ //# sourceMappingURL=switchbotClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"switchbotClient.js","sourceRoot":"","sources":["switchbotClient.ts"],"names":[],"mappings":"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"}
@@ -13,7 +13,7 @@ export interface ISwitchBotClient {
13
13
  }
14
14
 
15
15
  /**
16
- * Thin wrapper around node-switchbot v4.0.0-beta.2+
16
+ * Thin wrapper around node-switchbot v4.0.0+
17
17
  * Leverages upstream resilience features (retry, circuit breaker, connection intelligence)
18
18
  * while maintaining plugin-specific features like write debouncing and OpenAPI fallback.
19
19
  */
@@ -21,6 +21,8 @@ export class SwitchBotClient implements ISwitchBotClient {
21
21
  private cfg: SwitchBotPluginConfig
22
22
  private client: SwitchBot | null = null
23
23
  private writeDebounceMs = 100
24
+ private discoveryCacheTtlMs = 30_000
25
+ private lastDiscoveryAt = 0
24
26
  private logger: import('homebridge').Logger
25
27
  private pendingWrites: Map<string, { timer: any, body: any, resolvers: Array<{ resolve: (v: any) => void, reject: (e: any) => void }> }> = new Map()
26
28
 
@@ -33,24 +35,34 @@ export class SwitchBotClient implements ISwitchBotClient {
33
35
  if (typeof (cfg as any)?.writeDebounceMs === 'number') {
34
36
  this.writeDebounceMs = (cfg as any).writeDebounceMs
35
37
  }
38
+ if (typeof (cfg as any)?.discoveryCacheTtlMs === 'number') {
39
+ this.discoveryCacheTtlMs = Math.max(0, (cfg as any).discoveryCacheTtlMs)
40
+ }
36
41
  }
37
42
 
38
43
  async init(): Promise<void> {
44
+ if (this.client) {
45
+ return
46
+ }
47
+
39
48
  try {
40
49
  // Dynamic import of node-switchbot v4 with native resilience features
41
50
  const { SwitchBot } = await import('node-switchbot')
51
+ const rawNodeClientConfig = typeof (this.cfg as any)?.nodeClientConfig === 'object' ? (this.cfg as any).nodeClientConfig : {}
52
+ const scanTimeout = this.resolveScanTimeoutMs(rawNodeClientConfig)
42
53
  this.client = new SwitchBot({
43
54
  token: this.cfg.openApiToken,
44
55
  secret: this.cfg.openApiSecret,
45
- // Enable all built-in resilience features from node-switchbot v4
56
+ // Enable built-in resilience features from node-switchbot v4.
46
57
  enableFallback: true, // Auto-fallback from BLE to API
47
58
  enableRetry: true, // Retry with exponential backoff
48
59
  enableCircuitBreaker: true, // Circuit breaker per connection type
49
- enableMetrics: true, // Connection tracking and statistics
60
+ enableConnectionIntelligence: true, // Connection tracking and route preference
50
61
  enableBLE: this.cfg.enableBLE !== false, // Use config value, default true
51
- scanDuration: 5000, // BLE scan duration in milliseconds
52
- ...(typeof (this.cfg as any)?.nodeClientConfig === 'object' && (this.cfg as any).nodeClientConfig),
62
+ scanTimeout,
63
+ ...rawNodeClientConfig,
53
64
  })
65
+ this.lastDiscoveryAt = 0
54
66
  this.logger?.info?.('SwitchBot client initialized with native resilience features')
55
67
  } catch (e) {
56
68
  this.logger?.warn?.('Failed to load node-switchbot; will use OpenAPI fallback:', e)
@@ -61,8 +73,19 @@ export class SwitchBotClient implements ISwitchBotClient {
61
73
  async getDevice(id: string): Promise<any> {
62
74
  if (this.client) {
63
75
  try {
64
- const devices = await this.client.discover()
65
- return devices.find((d: any) => d.id === id)
76
+ const fromManager = this.getManagedDevice(id)
77
+ if (fromManager) {
78
+ return fromManager
79
+ }
80
+
81
+ const devices = await this.ensureDiscovered(false)
82
+ const fromDiscovery = devices.find((d: any) => d.id === id)
83
+ if (fromDiscovery) {
84
+ return fromDiscovery
85
+ }
86
+
87
+ const refreshDevices = await this.ensureDiscovered(true)
88
+ return refreshDevices.find((d: any) => d.id === id)
66
89
  } catch (e: any) {
67
90
  if (e instanceof SwitchbotAuthenticationError) {
68
91
  this.logger?.error?.(`Authentication error for getDevice(${id}):`, e.message)
@@ -85,7 +108,11 @@ export class SwitchBotClient implements ISwitchBotClient {
85
108
  async getDevices(): Promise<any[]> {
86
109
  if (this.client) {
87
110
  try {
88
- return await this.client.discover()
111
+ const fromManager = this.getManagedDevices()
112
+ if (fromManager.length > 0) {
113
+ return fromManager
114
+ }
115
+ return await this.ensureDiscovered(false)
89
116
  } catch (e) {
90
117
  this.logger?.warn?.('Client getDevices failed:', e)
91
118
  throw e
@@ -139,8 +166,7 @@ export class SwitchBotClient implements ISwitchBotClient {
139
166
  throw new SwitchbotOperationError('No SwitchBot client available for setDeviceState', 'no_client')
140
167
  }
141
168
  try {
142
- const devices = await this.client.discover()
143
- const device = devices.find((d: any) => d.id === id)
169
+ const device = await this.getDevice(id)
144
170
  if (!device) {
145
171
  throw new SwitchbotOperationError(`Device ${id} not found`, 'device_not_found')
146
172
  }
@@ -173,9 +199,68 @@ export class SwitchBotClient implements ISwitchBotClient {
173
199
  }
174
200
 
175
201
  async destroy(): Promise<void> {
202
+ for (const [, pending] of this.pendingWrites) {
203
+ clearTimeout(pending.timer)
204
+ const err = new SwitchbotOperationError('Client destroyed before pending write was sent', 'client_destroyed')
205
+ for (const r of pending.resolvers) {
206
+ r.reject(err)
207
+ }
208
+ }
209
+ this.pendingWrites.clear()
210
+
176
211
  if (this.client?.cleanup) {
177
212
  await this.client.cleanup()
178
213
  }
179
214
  this.client = null
215
+ this.lastDiscoveryAt = 0
216
+ }
217
+
218
+ private resolveScanTimeoutMs(rawNodeClientConfig: Record<string, any>): number {
219
+ if (typeof rawNodeClientConfig.scanTimeout === 'number' && Number.isFinite(rawNodeClientConfig.scanTimeout)) {
220
+ return Math.max(500, rawNodeClientConfig.scanTimeout)
221
+ }
222
+
223
+ if (typeof rawNodeClientConfig.scanDuration === 'number' && Number.isFinite(rawNodeClientConfig.scanDuration)) {
224
+ return Math.max(500, rawNodeClientConfig.scanDuration)
225
+ }
226
+
227
+ if (typeof (this.cfg as any)?.bleScanDurationSeconds === 'number') {
228
+ return Math.max(500, (this.cfg as any).bleScanDurationSeconds * 1000)
229
+ }
230
+
231
+ return 5000
232
+ }
233
+
234
+ private getManagedDevice(id: string): any {
235
+ const manager = (this.client as any)?.devices
236
+ if (manager?.get) {
237
+ return manager.get(id)
238
+ }
239
+ return undefined
240
+ }
241
+
242
+ private getManagedDevices(): any[] {
243
+ const manager = (this.client as any)?.devices
244
+ if (manager?.list) {
245
+ const list = manager.list()
246
+ return Array.isArray(list) ? list : []
247
+ }
248
+ return []
249
+ }
250
+
251
+ private async ensureDiscovered(force: boolean): Promise<any[]> {
252
+ if (!this.client) {
253
+ throw new SwitchbotOperationError('No SwitchBot client available', 'no_client')
254
+ }
255
+
256
+ const fromManager = this.getManagedDevices()
257
+ const cacheValid = this.discoveryCacheTtlMs > 0 && (Date.now() - this.lastDiscoveryAt) < this.discoveryCacheTtlMs
258
+ if (!force && cacheValid && fromManager.length > 0) {
259
+ return fromManager
260
+ }
261
+
262
+ const discovered = await this.client.discover()
263
+ this.lastDiscoveryAt = Date.now()
264
+ return discovered
180
265
  }
181
266
  }
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from 'vitest'
1
+ import { describe, expect, it, vi } from 'vitest'
2
2
 
3
3
  import { SwitchBotClient } from '../../src/switchbotClient'
4
4
 
@@ -20,4 +20,45 @@ describe('switchBotClient', () => {
20
20
  const client = new SwitchBotClient(cfg as any)
21
21
  expect((client as any).writeDebounceMs).toBe(321)
22
22
  })
23
+
24
+ it('should prefer managed devices before discovery', async () => {
25
+ const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }
26
+ const client = new SwitchBotClient({ logger } as any)
27
+
28
+ const managedDevices = [{ id: 'managed-1' }]
29
+ const discover = vi.fn().mockResolvedValue([{ id: 'discovered-1' }])
30
+ ;(client as any).client = {
31
+ devices: {
32
+ list: () => managedDevices,
33
+ get: () => managedDevices[0],
34
+ },
35
+ discover,
36
+ }
37
+
38
+ const devices = await client.getDevices()
39
+ const device = await client.getDevice('managed-1')
40
+
41
+ expect(devices).toEqual(managedDevices)
42
+ expect(device).toEqual(managedDevices[0])
43
+ expect(discover).not.toHaveBeenCalled()
44
+ })
45
+
46
+ it('should discover device when manager does not have it', async () => {
47
+ const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }
48
+ const client = new SwitchBotClient({ logger } as any)
49
+
50
+ const discovered = { id: 'abc123' }
51
+ const discover = vi.fn().mockResolvedValue([discovered])
52
+ ;(client as any).client = {
53
+ devices: {
54
+ list: () => [],
55
+ get: () => undefined,
56
+ },
57
+ discover,
58
+ }
59
+
60
+ const out = await client.getDevice('abc123')
61
+ expect(out).toEqual(discovered)
62
+ expect(discover).toHaveBeenCalledTimes(1)
63
+ })
23
64
  })
@@ -1,6 +1,7 @@
1
1
  import { execFile } from 'node:child_process'
2
2
  import path from 'node:path'
3
3
  import { promisify } from 'node:util'
4
+
4
5
  import { describe, it } from 'vitest'
5
6
 
6
7
  const execFileP = promisify(execFile)
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "declaration": false,
5
+ "declarationMap": false,
6
+ "outDir": "dist/homebridge-ui"
7
+ },
8
+ "include": [
9
+ "src/homebridge-ui/server.ts"
10
+ ]
11
+ }