@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.
- package/.github/workflows/release.yml +63 -15
- package/.github/workflows/stale.yml +2 -4
- package/CHANGELOG.md +21 -29
- package/MIGRATION.md +6 -6
- package/README.md +5 -3
- package/dist/device-types.js +7 -7
- package/dist/device-types.js.map +1 -1
- package/dist/deviceFactory.d.ts +1 -1
- package/dist/deviceFactory.d.ts.map +1 -1
- package/dist/deviceFactory.js +20 -20
- package/dist/deviceFactory.js.map +1 -1
- package/dist/homebridge-ui/device-types.js +246 -0
- package/dist/homebridge-ui/device-types.js.map +1 -0
- package/dist/homebridge-ui/deviceCommandMapper.js +319 -0
- package/dist/homebridge-ui/deviceCommandMapper.js.map +1 -0
- package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/discovery.js +5 -1
- package/dist/homebridge-ui/endpoints/discovery.js.map +1 -1
- package/dist/homebridge-ui/errors.js +32 -0
- package/dist/homebridge-ui/errors.js.map +1 -0
- package/dist/homebridge-ui/homebridge-ui/endpoints/config.js +90 -0
- package/dist/homebridge-ui/homebridge-ui/endpoints/config.js.map +1 -0
- package/dist/homebridge-ui/homebridge-ui/endpoints/devices.js +144 -0
- package/dist/homebridge-ui/homebridge-ui/endpoints/devices.js.map +1 -0
- package/dist/homebridge-ui/homebridge-ui/endpoints/discovery.js +219 -0
- package/dist/homebridge-ui/homebridge-ui/endpoints/discovery.js.map +1 -0
- package/dist/homebridge-ui/homebridge-ui/server.js +11 -0
- package/dist/homebridge-ui/homebridge-ui/server.js.map +1 -0
- package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js +108 -0
- package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js.map +1 -0
- package/dist/homebridge-ui/homebridge-ui/utils/device-migration.js +111 -0
- package/dist/homebridge-ui/homebridge-ui/utils/device-migration.js.map +1 -0
- package/dist/homebridge-ui/homebridge-ui/utils/logger.js +17 -0
- package/dist/homebridge-ui/homebridge-ui/utils/logger.js.map +1 -0
- package/dist/homebridge-ui/public/js/app.js +5 -9
- package/dist/homebridge-ui/public/js/app.js.map +2 -2
- package/dist/homebridge-ui/settings.js +8 -0
- package/dist/homebridge-ui/settings.js.map +1 -0
- package/dist/homebridge-ui/switchbotClient.js +247 -0
- package/dist/homebridge-ui/switchbotClient.js.map +1 -0
- package/dist/switchbotClient.d.ts +7 -1
- package/dist/switchbotClient.d.ts.map +1 -1
- package/dist/switchbotClient.js +82 -10
- package/dist/switchbotClient.js.map +1 -1
- package/docs/assets/main.js +1 -1
- package/docs/index.html +10 -4
- package/docs/variables/default.html +1 -1
- package/eslint.config.js +9 -10
- package/package.json +25 -23
- package/src/device-types.js +246 -0
- package/src/device-types.js.map +1 -0
- package/src/device-types.ts +7 -7
- package/src/deviceCommandMapper.js +319 -0
- package/src/deviceCommandMapper.js.map +1 -0
- package/src/deviceFactory.ts +22 -21
- package/src/errors.js +32 -0
- package/src/errors.js.map +1 -0
- package/src/homebridge-ui/endpoints/discovery.ts +5 -1
- package/src/settings.js +8 -0
- package/src/settings.js.map +1 -0
- package/src/switchbotClient.js +247 -0
- package/src/switchbotClient.js.map +1 -0
- package/src/switchbotClient.ts +95 -10
- package/test/client/switchbotClient.spec.ts +42 -1
- package/test/e2e/run-e2e.spec.ts +1 -0
- package/tsconfig.ui.json +11 -0
- package/.github/workflows/beta-release.yml +0 -52
package/src/deviceFactory.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
58
|
+
'smart radiator thermostat': GenericDevice,
|
|
58
59
|
'woiosensor': GenericDevice,
|
|
59
|
-
'garage door opener': GenericDevice,
|
|
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,
|
|
69
|
-
'plug mini (jp)': PlugMiniDevice,
|
|
70
|
-
'plug mini (us)': PlugMiniDevice,
|
|
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,
|
|
81
|
-
'hub mini': GenericDevice,
|
|
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, //
|
|
89
|
-
'roller shade': RollerShadeDevice,
|
|
90
|
-
'strip light 3': StripLightDevice,
|
|
91
|
-
'circulator fan': FanDevice,
|
|
92
|
-
'smart lock pro': LockDevice,
|
|
93
|
-
'lock lite': LockDevice,
|
|
94
|
-
'keypad': LockDevice,
|
|
95
|
-
'lock vision pro': LockDevice,
|
|
96
|
-
'floor lamp': LightDevice,
|
|
97
|
-
'rgbicww floor lamp': LightStripDevice,
|
|
98
|
-
'rgbicww strip light': LightStripDevice,
|
|
99
|
-
'home climate panel': GenericDevice, //
|
|
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>()
|
package/src/settings.js
ADDED
|
@@ -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":"AAGA,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAClE,OAAO,EAAE,0BAA0B,EAAE,4BAA4B,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAU/G;;;;GAIG;AACH,MAAM,OAAO,eAAe;IAClB,GAAG,CAAuB;IAC1B,MAAM,GAAqB,IAAI,CAAA;IAC/B,eAAe,GAAG,GAAG,CAAA;IACrB,mBAAmB,GAAG,MAAM,CAAA;IAC5B,eAAe,GAAG,CAAC,CAAA;IACnB,MAAM,CAA6B;IACnC,aAAa,GAAsH,IAAI,GAAG,EAAE,CAAA;IAEpJ,YAAY,GAA0B;QACpC,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;QACd,IAAI,CAAC,MAAM,GAAI,GAAW,EAAE,MAAqC,CAAA;QACjE,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAA;QACpF,CAAC;QACD,IAAI,OAAQ,GAAW,EAAE,eAAe,KAAK,QAAQ,EAAE,CAAC;YACtD,IAAI,CAAC,eAAe,GAAI,GAAW,CAAC,eAAe,CAAA;QACrD,CAAC;QACD,IAAI,OAAQ,GAAW,EAAE,mBAAmB,KAAK,QAAQ,EAAE,CAAC;YAC1D,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAG,GAAW,CAAC,mBAAmB,CAAC,CAAA;QAC1E,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI;QACR,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,OAAM;QACR,CAAC;QAED,IAAI,CAAC;YACH,sEAAsE;YACtE,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAAA;YACpD,MAAM,mBAAmB,GAAG,OAAQ,IAAI,CAAC,GAAW,EAAE,gBAAgB,KAAK,QAAQ,CAAC,CAAC,CAAE,IAAI,CAAC,GAAW,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAA;YAC7H,MAAM,WAAW,GAAG,IAAI,CAAC,oBAAoB,CAAC,mBAAmB,CAAC,CAAA;YAClE,IAAI,CAAC,MAAM,GAAG,IAAI,SAAS,CAAC;gBAC1B,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,YAAY;gBAC5B,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,aAAa;gBAC9B,8DAA8D;gBAC9D,cAAc,EAAE,IAAI,EAAE,gCAAgC;gBACtD,WAAW,EAAE,IAAI,EAAE,iCAAiC;gBACpD,oBAAoB,EAAE,IAAI,EAAE,sCAAsC;gBAClE,4BAA4B,EAAE,IAAI,EAAE,2CAA2C;gBAC/E,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,SAAS,KAAK,KAAK,EAAE,iCAAiC;gBAC1E,WAAW;gBACX,GAAG,mBAAmB;aACvB,CAAC,CAAA;YACF,IAAI,CAAC,eAAe,GAAG,CAAC,CAAA;YACxB,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,8DAA8D,CAAC,CAAA;QACrF,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,2DAA2D,EAAE,CAAC,CAAC,CAAA;YACnF,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;QACpB,CAAC;IACH,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,EAAU;QACxB,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC;gBACH,MAAM,WAAW,GAAG,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAA;gBAC7C,IAAI,WAAW,EAAE,CAAC;oBAChB,OAAO,WAAW,CAAA;gBACpB,CAAC;gBAED,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAA;gBAClD,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAA;gBAC3D,IAAI,aAAa,EAAE,CAAC;oBAClB,OAAO,aAAa,CAAA;gBACtB,CAAC;gBAED,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAA;gBACxD,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAA;YACrD,CAAC;YAAC,OAAO,CAAM,EAAE,CAAC;gBAChB,IAAI,CAAC,YAAY,4BAA4B,EAAE,CAAC;oBAC9C,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,sCAAsC,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;oBAC7E,MAAM,CAAC,CAAA;gBACT,CAAC;qBAAM,IAAI,CAAC,YAAY,uBAAuB,EAAE,CAAC;oBAChD,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,iCAAiC,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAA;oBAC/E,MAAM,CAAC,CAAA;gBACT,CAAC;qBAAM,IAAI,CAAC,YAAY,0BAA0B,EAAE,CAAC;oBACnD,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,wCAAwC,EAAE,IAAI,EAAE,CAAC,CAAC,cAAc,CAAC,CAAA;oBACrF,MAAM,CAAC,CAAA;gBACT,CAAC;qBAAM,CAAC;oBACN,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE,CAAC,CAAC,CAAA;oBAC5D,MAAM,CAAC,CAAA;gBACT,CAAC;YACH,CAAC;QACH,CAAC;QACD,MAAM,IAAI,uBAAuB,CAAC,+BAA+B,EAAE,WAAW,CAAC,CAAA;IACjF,CAAC;IAED,KAAK,CAAC,UAAU;QACd,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC;gBACH,MAAM,WAAW,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAA;gBAC5C,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC3B,OAAO,WAAW,CAAA;gBACpB,CAAC;gBACD,OAAO,MAAM,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAA;YAC3C,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,2BAA2B,EAAE,CAAC,CAAC,CAAA;gBACnD,MAAM,CAAC,CAAA;YACT,CAAC;QACH,CAAC;QACD,MAAM,IAAI,uBAAuB,CAAC,+BAA+B,EAAE,WAAW,CAAC,CAAA;IACjF,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,EAAU,EAAE,IAAS;QACxC,0DAA0D;QAC1D,IAAI,CAAC,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC,eAAe,IAAI,CAAC,EAAE,CAAC;YACvD,OAAO,IAAI,CAAC,iBAAiB,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;QACzC,CAAC;QAED,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YAC3C,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAA;gBACpB,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAA;gBAC5C,OAAM;YACR,CAAC;YAED,MAAM,SAAS,GAAmE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAA;YACvG,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,IAAI,EAAE;gBAClC,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;gBACxC,IAAI,CAAC,KAAK,EAAE,CAAC;oBACX,OAAM;gBACR,CAAC;gBACD,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;gBAC7B,IAAI,CAAC;oBACH,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;oBACxD,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,SAAS;wBAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;gBACjD,CAAC;gBAAC,OAAO,CAAM,EAAE,CAAC;oBAChB,IAAI,CAAC,YAAY,4BAA4B,EAAE,CAAC;wBAC9C,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,2CAA2C,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;oBACpF,CAAC;yBAAM,IAAI,CAAC,YAAY,uBAAuB,EAAE,CAAC;wBAChD,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,sCAAsC,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAA;oBACtF,CAAC;yBAAM,IAAI,CAAC,YAAY,0BAA0B,EAAE,CAAC;wBACnD,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,6CAA6C,EAAE,IAAI,EAAE,CAAC,CAAC,cAAc,CAAC,CAAA;oBAC5F,CAAC;oBACD,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,SAAS;wBAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;gBAC9C,CAAC;YACH,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC,CAAA;YAExB,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAA;QACxD,CAAC,CAAC,CAAA;IACJ,CAAC;IAEO,KAAK,CAAC,iBAAiB,CAAC,EAAU,EAAE,IAAS;QACnD,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,MAAM,IAAI,uBAAuB,CAAC,kDAAkD,EAAE,WAAW,CAAC,CAAA;QACpG,CAAC;QACD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAA;YACvC,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,IAAI,uBAAuB,CAAC,UAAU,EAAE,YAAY,EAAE,kBAAkB,CAAC,CAAA;YACjF,CAAC;YACD,MAAM,UAAU,GAAG,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;YAC1D,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,CAAA;YAC7B,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,MAAM,IAAI,uBAAuB,CAAC,8BAA8B,EAAE,YAAY,CAAC,CAAA;YACjF,CAAC;YACD,MAAM,OAAO,GAAG,uBAAuB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAA;YAC5D,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,MAAM,IAAI,uBAAuB,CAAC,wBAAwB,OAAO,sBAAsB,UAAU,GAAG,EAAE,qBAAqB,CAAC,CAAA;YAC9H,CAAC;YACD,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,6BAA6B,OAAO,sBAAsB,UAAU,GAAG,CAAC,CAAA;YACnG,OAAO,MAAM,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;QACpC,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,IAAI,CAAC,YAAY,4BAA4B,EAAE,CAAC;gBAC9C,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,2CAA2C,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;gBAClF,MAAM,CAAC,CAAA;YACT,CAAC;iBAAM,IAAI,CAAC,YAAY,uBAAuB,EAAE,CAAC;gBAChD,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,sCAAsC,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAA;gBACpF,MAAM,CAAC,CAAA;YACT,CAAC;iBAAM,IAAI,CAAC,YAAY,0BAA0B,EAAE,CAAC;gBACnD,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,6CAA6C,EAAE,IAAI,EAAE,CAAC,CAAC,cAAc,CAAC,CAAA;gBAC1F,MAAM,CAAC,CAAA;YACT,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE,CAAC,CAAC,CAAA;gBAC1D,MAAM,CAAC,CAAA;YACT,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAO;QACX,KAAK,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YAC7C,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;YAC3B,MAAM,GAAG,GAAG,IAAI,uBAAuB,CAAC,gDAAgD,EAAE,kBAAkB,CAAC,CAAA;YAC7G,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;gBAClC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YACf,CAAC;QACH,CAAC;QACD,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAA;QAE1B,IAAI,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;YACzB,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAA;QAC7B,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;QAClB,IAAI,CAAC,eAAe,GAAG,CAAC,CAAA;IAC1B,CAAC;IAEO,oBAAoB,CAAC,mBAAwC;QACnE,IAAI,OAAO,mBAAmB,CAAC,WAAW,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,WAAW,CAAC,EAAE,CAAC;YAC5G,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAA;QACvD,CAAC;QAED,IAAI,OAAO,mBAAmB,CAAC,YAAY,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,YAAY,CAAC,EAAE,CAAC;YAC9G,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,mBAAmB,CAAC,YAAY,CAAC,CAAA;QACxD,CAAC;QAED,IAAI,OAAQ,IAAI,CAAC,GAAW,EAAE,sBAAsB,KAAK,QAAQ,EAAE,CAAC;YAClE,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAG,IAAI,CAAC,GAAW,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAA;QACvE,CAAC;QAED,OAAO,IAAI,CAAA;IACb,CAAC;IAEO,gBAAgB,CAAC,EAAU;QACjC,MAAM,OAAO,GAAI,IAAI,CAAC,MAAc,EAAE,OAAO,CAAA;QAC7C,IAAI,OAAO,EAAE,GAAG,EAAE,CAAC;YACjB,OAAO,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACxB,CAAC;QACD,OAAO,SAAS,CAAA;IAClB,CAAC;IAEO,iBAAiB;QACvB,MAAM,OAAO,GAAI,IAAI,CAAC,MAAc,EAAE,OAAO,CAAA;QAC7C,IAAI,OAAO,EAAE,IAAI,EAAE,CAAC;YAClB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAA;YAC3B,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;QACxC,CAAC;QACD,OAAO,EAAE,CAAA;IACX,CAAC;IAEO,KAAK,CAAC,gBAAgB,CAAC,KAAc;QAC3C,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,MAAM,IAAI,uBAAuB,CAAC,+BAA+B,EAAE,WAAW,CAAC,CAAA;QACjF,CAAC;QAED,MAAM,WAAW,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAA;QAC5C,MAAM,UAAU,GAAG,IAAI,CAAC,mBAAmB,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,IAAI,CAAC,mBAAmB,CAAA;QACjH,IAAI,CAAC,KAAK,IAAI,UAAU,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACnD,OAAO,WAAW,CAAA;QACpB,CAAC;QAED,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAA;QAC/C,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;QACjC,OAAO,UAAU,CAAA;IACnB,CAAC;CACF"}
|
package/src/switchbotClient.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface ISwitchBotClient {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Thin wrapper around node-switchbot v4.0.0
|
|
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
|
|
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
|
-
|
|
60
|
+
enableConnectionIntelligence: true, // Connection tracking and route preference
|
|
50
61
|
enableBLE: this.cfg.enableBLE !== false, // Use config value, default true
|
|
51
|
-
|
|
52
|
-
...
|
|
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
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
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
|
})
|
package/test/e2e/run-e2e.spec.ts
CHANGED