@switchbot/homebridge-switchbot 5.0.0-beta.74 → 5.0.0-beta.76
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/dist/homebridge-ui/public/index.html +16 -9
- package/dist/homebridge-ui/server.d.ts.map +1 -1
- package/dist/homebridge-ui/server.js +10 -2
- package/dist/homebridge-ui/server.js.map +1 -1
- package/dist/switchbotClient.d.ts +9 -9
- package/dist/switchbotClient.d.ts.map +1 -1
- package/dist/switchbotClient.js +78 -143
- package/dist/switchbotClient.js.map +1 -1
- package/docs/variables/default.html +1 -1
- package/eslint.config.js +2 -8
- package/package.json +9 -17
- package/src/homebridge-ui/public/index.html +16 -9
- package/src/homebridge-ui/server.ts +13 -3
- package/src/switchbotClient.ts +78 -141
|
@@ -59,11 +59,11 @@
|
|
|
59
59
|
async function loadCredentialStatus() {
|
|
60
60
|
try {
|
|
61
61
|
const resp = await homebridge.request('/credentials', {})
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
console.log('Load credentials response:', resp)
|
|
63
|
+
|
|
64
|
+
if (!resp || resp.success === false) {
|
|
65
|
+
console.error('Failed to load credentials:', resp)
|
|
66
|
+
return
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const creds = resp.data || {}
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
tokenStatus.classList.add('ok')
|
|
76
76
|
} else {
|
|
77
77
|
tokenStatus.textContent = 'Not configured'
|
|
78
|
+
tokenStatus.classList.remove('ok')
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
if (creds.hasSecret) {
|
|
@@ -82,6 +83,7 @@
|
|
|
82
83
|
secretStatus.classList.add('ok')
|
|
83
84
|
} else {
|
|
84
85
|
secretStatus.textContent = 'Not configured'
|
|
86
|
+
secretStatus.classList.remove('ok')
|
|
85
87
|
}
|
|
86
88
|
} catch (e) {
|
|
87
89
|
console.error('Error loading credentials:', e)
|
|
@@ -104,12 +106,16 @@
|
|
|
104
106
|
saveBtn.disabled = true
|
|
105
107
|
saveBtn.textContent = 'Saving...'
|
|
106
108
|
|
|
109
|
+
console.log('Saving credentials...')
|
|
107
110
|
const resp = await homebridge.request('/credentials', { token, secret })
|
|
111
|
+
console.log('Save response:', resp)
|
|
112
|
+
|
|
108
113
|
if (!resp || resp.success === false) {
|
|
109
|
-
throw new Error(resp?.
|
|
114
|
+
throw new Error(resp?.message || 'Save failed')
|
|
110
115
|
}
|
|
111
116
|
|
|
112
|
-
|
|
117
|
+
const result = resp.data || resp
|
|
118
|
+
saveStatus.textContent = '✓ ' + (result.message || 'Credentials saved successfully')
|
|
113
119
|
saveStatus.classList.remove('error')
|
|
114
120
|
saveStatus.classList.add('success-msg')
|
|
115
121
|
|
|
@@ -117,8 +123,8 @@
|
|
|
117
123
|
document.getElementById('token').value = ''
|
|
118
124
|
document.getElementById('secret').value = ''
|
|
119
125
|
|
|
120
|
-
// Reload status
|
|
121
|
-
setTimeout(() => loadCredentialStatus(),
|
|
126
|
+
// Reload status to verify save
|
|
127
|
+
setTimeout(() => loadCredentialStatus(), 500)
|
|
122
128
|
|
|
123
129
|
// Clear status message
|
|
124
130
|
setTimeout(() => {
|
|
@@ -126,6 +132,7 @@
|
|
|
126
132
|
saveStatus.classList.remove('success-msg')
|
|
127
133
|
}, 3000)
|
|
128
134
|
} catch (e) {
|
|
135
|
+
console.error('Save error:', e)
|
|
129
136
|
saveStatus.textContent = 'Error: ' + (e?.message || 'Failed to save')
|
|
130
137
|
saveStatus.classList.add('error')
|
|
131
138
|
} finally {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/homebridge-ui/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/homebridge-ui/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAgB,MAAM,6BAA6B,CAAA;AAGpF,QAAA,MAAM,MAAM,0BAAiC,CAAA;AA2H7C,eAAe,MAAM,CAAA"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
2
|
import { HomebridgePluginUiServer, RequestError } from '@homebridge/plugin-ui-utils';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
3
4
|
const server = new HomebridgePluginUiServer();
|
|
4
5
|
// Helper function to find the SwitchBot platform config
|
|
5
6
|
async function getSwitchBotPlatformConfig() {
|
|
@@ -68,12 +69,14 @@ server.onRequest('/credentials', async (body) => {
|
|
|
68
69
|
if (!body || Object.keys(body).length === 0) {
|
|
69
70
|
// GET request - return current status
|
|
70
71
|
const { platform } = await getSwitchBotPlatformConfig();
|
|
71
|
-
|
|
72
|
+
const status = {
|
|
72
73
|
hasToken: !!platform.openApiToken,
|
|
73
74
|
hasSecret: !!platform.openApiSecret,
|
|
74
75
|
tokenLength: platform.openApiToken ? String(platform.openApiToken).length : 0,
|
|
75
76
|
secretLength: platform.openApiSecret ? String(platform.openApiSecret).length : 0,
|
|
76
77
|
};
|
|
78
|
+
console.log('[SwitchBot UI] GET /credentials - Status:', status);
|
|
79
|
+
return status;
|
|
77
80
|
}
|
|
78
81
|
else {
|
|
79
82
|
// POST request - save credentials
|
|
@@ -82,11 +85,15 @@ server.onRequest('/credentials', async (body) => {
|
|
|
82
85
|
throw new Error('Token and secret are required');
|
|
83
86
|
}
|
|
84
87
|
const { config, platform, cfgPath } = await getSwitchBotPlatformConfig();
|
|
88
|
+
console.log('[SwitchBot UI] POST /credentials - Saving to platform:', platform.platform || platform.name);
|
|
89
|
+
console.log('[SwitchBot UI] Config path:', cfgPath);
|
|
90
|
+
console.log('[SwitchBot UI] Token length:', token.length, 'Secret length:', secret.length);
|
|
85
91
|
// Save token and secret directly on the platform config
|
|
86
92
|
platform.openApiToken = token;
|
|
87
93
|
platform.openApiSecret = secret;
|
|
88
94
|
// Write back to config file
|
|
89
95
|
await fs.writeFile(cfgPath, JSON.stringify(config, null, 2), 'utf8');
|
|
96
|
+
console.log('[SwitchBot UI] Credentials saved successfully');
|
|
90
97
|
return {
|
|
91
98
|
success: true,
|
|
92
99
|
message: 'Credentials saved successfully',
|
|
@@ -94,6 +101,7 @@ server.onRequest('/credentials', async (body) => {
|
|
|
94
101
|
}
|
|
95
102
|
}
|
|
96
103
|
catch (e) {
|
|
104
|
+
console.error('[SwitchBot UI] Error in /credentials:', e);
|
|
97
105
|
throw new RequestError('Failed to handle credentials request', e);
|
|
98
106
|
}
|
|
99
107
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/homebridge-ui/server.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/homebridge-ui/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,29 +2,29 @@ import type { SwitchBotPluginConfig } from './settings.js';
|
|
|
2
2
|
export interface ISwitchBotClient {
|
|
3
3
|
init(): Promise<void>;
|
|
4
4
|
getDevice(id: string): Promise<any>;
|
|
5
|
+
getDevices(): Promise<any[]>;
|
|
6
|
+
setDeviceState(id: string, body: any): Promise<any>;
|
|
5
7
|
destroy(): Promise<void>;
|
|
6
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Thin wrapper around node-switchbot v4.0.0-beta.2+
|
|
11
|
+
* Leverages upstream resilience features (retry, circuit breaker, connection intelligence)
|
|
12
|
+
* while maintaining plugin-specific features like write debouncing and OpenAPI fallback.
|
|
13
|
+
*/
|
|
7
14
|
export declare class SwitchBotClient implements ISwitchBotClient {
|
|
8
15
|
private cfg;
|
|
9
16
|
private client;
|
|
10
17
|
private baseUrl;
|
|
11
18
|
private requestTimeout;
|
|
12
19
|
private maxRetries;
|
|
13
|
-
private baseBackoffMs;
|
|
14
|
-
private maxBackoffMs;
|
|
15
|
-
private backoffFactor;
|
|
16
|
-
private jitter;
|
|
17
20
|
private writeDebounceMs;
|
|
18
21
|
private logger;
|
|
19
|
-
private perDeviceRetries;
|
|
20
|
-
private perDeviceMaxRetries;
|
|
21
|
-
private perDeviceCooldownMs;
|
|
22
|
-
private perDeviceCooldownUntil;
|
|
23
|
-
constructor(cfg: SwitchBotPluginConfig);
|
|
24
22
|
private pendingWrites;
|
|
23
|
+
constructor(cfg: SwitchBotPluginConfig);
|
|
25
24
|
init(): Promise<void>;
|
|
26
25
|
private fetchWithTimeoutAndRetry;
|
|
27
26
|
getDevice(id: string): Promise<any>;
|
|
27
|
+
getDevices(): Promise<any[]>;
|
|
28
28
|
setDeviceState(id: string, body: any): Promise<any>;
|
|
29
29
|
private _doSetDeviceState;
|
|
30
30
|
destroy(): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"switchbotClient.d.ts","sourceRoot":"","sources":["../src/switchbotClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAE1D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;IACnC,
|
|
1
|
+
{"version":3,"file":"switchbotClient.d.ts","sourceRoot":"","sources":["../src/switchbotClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAE1D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;IACnC,UAAU,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;IAC5B,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;IACnD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACzB;AAED;;;;GAIG;AACH,qBAAa,eAAgB,YAAW,gBAAgB;IACtD,OAAO,CAAC,GAAG,CAAuB;IAClC,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,OAAO,CAAoC;IACnD,OAAO,CAAC,cAAc,CAAO;IAC7B,OAAO,CAAC,UAAU,CAAI;IACtB,OAAO,CAAC,eAAe,CAAM;IAC7B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,aAAa,CAA0H;gBAEnI,GAAG,EAAE,qBAAqB;IAQhC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAqBb,wBAAwB;IAkChC,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAmBnC,UAAU,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAoB5B,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;YA+B3C,iBAAiB;IAqCzB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAM/B"}
|
package/dist/switchbotClient.js
CHANGED
|
@@ -1,26 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Thin wrapper around node-switchbot v4.0.0-beta.2+
|
|
3
|
+
* Leverages upstream resilience features (retry, circuit breaker, connection intelligence)
|
|
4
|
+
* while maintaining plugin-specific features like write debouncing and OpenAPI fallback.
|
|
5
|
+
*/
|
|
2
6
|
export class SwitchBotClient {
|
|
3
7
|
cfg;
|
|
4
8
|
client = null;
|
|
5
9
|
baseUrl = 'https://api.switch-bot.com/v1.0';
|
|
6
|
-
// configurable network options
|
|
7
10
|
requestTimeout = 5000;
|
|
8
11
|
maxRetries = 2;
|
|
9
|
-
baseBackoffMs = 100;
|
|
10
|
-
maxBackoffMs = 2000;
|
|
11
|
-
backoffFactor = 2;
|
|
12
|
-
jitter = true;
|
|
13
|
-
// per-device write debounce (ms). 0 = disabled. Configurable via plugin options.
|
|
14
|
-
// Default enabled to coalesce rapid writes and prevent command floods.
|
|
15
12
|
writeDebounceMs = 100;
|
|
16
|
-
// structured logger (expects info/warn/error/debug); defaults to console
|
|
17
13
|
logger;
|
|
18
|
-
|
|
19
|
-
perDeviceRetries = new Map();
|
|
20
|
-
perDeviceMaxRetries = 3;
|
|
21
|
-
// per-device cooldown after exceeding retries (ms)
|
|
22
|
-
perDeviceCooldownMs = 60_000;
|
|
23
|
-
perDeviceCooldownUntil = new Map();
|
|
14
|
+
pendingWrites = new Map();
|
|
24
15
|
constructor(cfg) {
|
|
25
16
|
this.cfg = cfg;
|
|
26
17
|
this.logger = cfg?.logger ?? console;
|
|
@@ -28,181 +19,115 @@ export class SwitchBotClient {
|
|
|
28
19
|
this.requestTimeout = cfg.requestTimeout;
|
|
29
20
|
if (typeof cfg?.maxRetries === 'number')
|
|
30
21
|
this.maxRetries = cfg.maxRetries;
|
|
31
|
-
if (typeof cfg?.perDeviceMaxRetries === 'number')
|
|
32
|
-
this.perDeviceMaxRetries = cfg.perDeviceMaxRetries;
|
|
33
|
-
if (typeof cfg?.baseBackoffMs === 'number')
|
|
34
|
-
this.baseBackoffMs = cfg.baseBackoffMs;
|
|
35
|
-
if (typeof cfg?.maxBackoffMs === 'number')
|
|
36
|
-
this.maxBackoffMs = cfg.maxBackoffMs;
|
|
37
|
-
if (typeof cfg?.backoffFactor === 'number')
|
|
38
|
-
this.backoffFactor = cfg.backoffFactor;
|
|
39
|
-
if (typeof cfg?.jitter === 'boolean')
|
|
40
|
-
this.jitter = cfg.jitter;
|
|
41
|
-
if (typeof cfg?.perDeviceCooldownMs === 'number')
|
|
42
|
-
this.perDeviceCooldownMs = cfg.perDeviceCooldownMs;
|
|
43
22
|
if (typeof cfg?.writeDebounceMs === 'number')
|
|
44
23
|
this.writeDebounceMs = cfg.writeDebounceMs;
|
|
45
24
|
}
|
|
46
|
-
// pending writes coalescing: deviceId -> { timer, body, resolvers }
|
|
47
|
-
pendingWrites = new Map();
|
|
48
25
|
async init() {
|
|
49
|
-
// Try to dynamically import the new node-switchbot package. If it exposes
|
|
50
|
-
// a hybrid client we will use it. Otherwise the consumer can implement
|
|
51
|
-
// OpenAPI fallback logic here.
|
|
52
26
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
27
|
+
// Dynamic import of node-switchbot v4 with native resilience features
|
|
28
|
+
const { SwitchBot } = await import('node-switchbot');
|
|
29
|
+
this.client = new SwitchBot({
|
|
30
|
+
token: this.cfg.openApiToken,
|
|
31
|
+
secret: this.cfg.openApiSecret,
|
|
32
|
+
// Enable all built-in resilience features from node-switchbot v4
|
|
33
|
+
enableFallback: true, // Auto-fallback from BLE to API
|
|
34
|
+
enableRetry: true, // Retry with exponential backoff
|
|
35
|
+
enableCircuitBreaker: true, // Circuit breaker per connection type
|
|
36
|
+
enableMetrics: true, // Connection tracking and statistics
|
|
37
|
+
...(typeof this.cfg?.nodeClientConfig === 'object' && this.cfg.nodeClientConfig),
|
|
38
|
+
});
|
|
39
|
+
this.logger?.info?.('SwitchBot client initialized with native resilience features');
|
|
61
40
|
}
|
|
62
41
|
catch (e) {
|
|
63
|
-
|
|
64
|
-
this.logger?.warn?.('node-switchbot import failed; falling back to OpenAPI when token available');
|
|
42
|
+
this.logger?.warn?.('Failed to load node-switchbot; will use OpenAPI fallback:', e);
|
|
65
43
|
this.client = null;
|
|
66
44
|
}
|
|
67
45
|
}
|
|
68
|
-
async fetchWithTimeoutAndRetry(url, opts = {}, timeoutMs, retries
|
|
46
|
+
async fetchWithTimeoutAndRetry(url, opts = {}, timeoutMs, retries) {
|
|
69
47
|
const to = typeof timeoutMs === 'number' ? timeoutMs : this.requestTimeout;
|
|
70
48
|
const max = typeof retries === 'number' ? retries : this.maxRetries;
|
|
71
|
-
// Check per-device cooldown
|
|
72
|
-
if (deviceId) {
|
|
73
|
-
const until = this.perDeviceCooldownUntil.get(deviceId) ?? 0;
|
|
74
|
-
if (Date.now() < until) {
|
|
75
|
-
const ms = until - Date.now();
|
|
76
|
-
this.logger?.warn?.('device in cooldown, aborting fetch', { deviceId, cooldownMs: ms });
|
|
77
|
-
throw new Error(`device ${deviceId} in cooldown for ${ms}ms`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
49
|
let attempt = 0;
|
|
81
50
|
while (true) {
|
|
82
51
|
attempt += 1;
|
|
83
|
-
// AbortController-based timeout
|
|
84
52
|
const controller = new AbortController();
|
|
85
53
|
const timer = setTimeout(() => controller.abort(), to);
|
|
86
54
|
try {
|
|
87
|
-
this.logger?.debug?.('fetch', { url, attempt, deviceId });
|
|
88
55
|
const fetchOpts = Object.assign({}, opts);
|
|
89
56
|
try {
|
|
90
57
|
Object.defineProperty(fetchOpts, 'signal', { value: controller.signal, enumerable: false, configurable: true });
|
|
91
58
|
}
|
|
92
59
|
catch (_e) {
|
|
93
|
-
// ignore
|
|
60
|
+
// ignore
|
|
94
61
|
}
|
|
95
62
|
const resp = await fetch(url, fetchOpts);
|
|
96
63
|
clearTimeout(timer);
|
|
97
|
-
|
|
98
|
-
const safeReadBody = async (r) => {
|
|
99
|
-
try {
|
|
100
|
-
if (!r)
|
|
101
|
-
return '';
|
|
102
|
-
if (typeof r.text === 'function')
|
|
103
|
-
return await r.text();
|
|
104
|
-
if (typeof r.json === 'function') {
|
|
105
|
-
const j = await r.json().catch(() => null);
|
|
106
|
-
return j ? JSON.stringify(j) : '';
|
|
107
|
-
}
|
|
108
|
-
return String(r);
|
|
109
|
-
}
|
|
110
|
-
catch (_e) {
|
|
111
|
-
return '';
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
// Some tests/mocks provide response-like objects without numeric `status`.
|
|
115
|
-
// In those cases attempt to parse the body and return a Response-like
|
|
116
|
-
// object so callers (resp.json()) continue to work instead of forcing
|
|
117
|
-
// a retry path.
|
|
118
|
-
if (typeof resp.status !== 'number') {
|
|
119
|
-
const bodyStr = await safeReadBody(resp);
|
|
120
|
-
try {
|
|
121
|
-
const parsed = bodyStr ? JSON.parse(bodyStr) : {};
|
|
122
|
-
if (deviceId)
|
|
123
|
-
this.perDeviceRetries.set(deviceId, 0);
|
|
124
|
-
return { ok: true, status: 200, json: async () => parsed };
|
|
125
|
-
}
|
|
126
|
-
catch (_e) {
|
|
127
|
-
if (deviceId)
|
|
128
|
-
this.perDeviceRetries.set(deviceId, 0);
|
|
129
|
-
return { ok: true, status: 200, json: async () => ({ body: bodyStr }) };
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
// classify response
|
|
133
|
-
if (resp.ok) {
|
|
134
|
-
if (deviceId)
|
|
135
|
-
this.perDeviceRetries.set(deviceId, 0);
|
|
64
|
+
if (resp.ok)
|
|
136
65
|
return resp;
|
|
66
|
+
// server error / rate limit: retry
|
|
67
|
+
if (resp.status >= 500 || resp.status === 429) {
|
|
68
|
+
throw new Error(`retriable response: ${resp.status}`);
|
|
137
69
|
}
|
|
138
|
-
|
|
139
|
-
if (resp.status >= 400 && resp.status < 500 && resp.status !== 429) {
|
|
140
|
-
const body = await safeReadBody(resp);
|
|
141
|
-
this.logger?.error?.('fetch non-retriable response', { url, status: resp.status, body });
|
|
142
|
-
const err = new Error(`HTTP ${resp.status}`);
|
|
143
|
-
err.status = resp.status;
|
|
144
|
-
throw err;
|
|
145
|
-
}
|
|
146
|
-
// server error / rate limit: treat as retriable
|
|
147
|
-
const body = await safeReadBody(resp);
|
|
148
|
-
this.logger?.warn?.('fetch retriable response', { url, status: resp.status, body });
|
|
149
|
-
// throw to enter retry/catch logic below and apply backoff
|
|
150
|
-
throw new Error(`retriable response: ${resp.status || 'unknown'}`);
|
|
70
|
+
return resp;
|
|
151
71
|
}
|
|
152
72
|
catch (err) {
|
|
153
73
|
clearTimeout(timer);
|
|
154
|
-
const isAbort = err?.name === 'AbortError' || err?.message === 'The user aborted a request.' || err?.message === 'timeout';
|
|
155
|
-
const reason = isAbort ? 'timeout' : err?.message ?? String(err);
|
|
156
|
-
this.logger?.warn?.('fetch failed', { url, attempt, deviceId, reason });
|
|
157
|
-
// per-device retry guard
|
|
158
|
-
if (deviceId) {
|
|
159
|
-
const prev = this.perDeviceRetries.get(deviceId) ?? 0;
|
|
160
|
-
const next = prev + 1;
|
|
161
|
-
this.perDeviceRetries.set(deviceId, next);
|
|
162
|
-
if (next > this.perDeviceMaxRetries) {
|
|
163
|
-
this.logger?.error?.('per-device retry limit exceeded, entering cooldown', { deviceId, attempts: next });
|
|
164
|
-
this.perDeviceCooldownUntil.set(deviceId, Date.now() + this.perDeviceCooldownMs);
|
|
165
|
-
throw new Error(`per-device retry limit exceeded for ${deviceId}`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
74
|
if (attempt > max)
|
|
169
75
|
throw err;
|
|
170
|
-
// exponential backoff
|
|
171
|
-
|
|
172
|
-
if (this.jitter) {
|
|
173
|
-
const jitterVal = Math.floor(Math.random() * backoff);
|
|
174
|
-
backoff = Math.floor(backoff / 2) + jitterVal;
|
|
175
|
-
}
|
|
176
|
-
this.logger?.debug?.('backoff before retry', { url, attempt, backoff });
|
|
76
|
+
// exponential backoff
|
|
77
|
+
const backoff = Math.min(100 * Math.pow(2, attempt - 1), 2000);
|
|
177
78
|
await new Promise((r) => setTimeout(r, backoff));
|
|
178
79
|
}
|
|
179
80
|
}
|
|
180
81
|
}
|
|
181
82
|
async getDevice(id) {
|
|
182
|
-
|
|
183
|
-
|
|
83
|
+
// Try client API first (with node-switchbot's smart fallback and retry)
|
|
84
|
+
if (this.client?.getDevice) {
|
|
85
|
+
try {
|
|
86
|
+
return await this.client.getDevice(id);
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
this.logger?.warn?.(`Client getDevice failed for ${id}, trying OpenAPI fallback:`, e);
|
|
90
|
+
}
|
|
184
91
|
}
|
|
185
|
-
// Fallback: call OpenAPI via HTTP
|
|
92
|
+
// Fallback: call OpenAPI via HTTP
|
|
186
93
|
if (this.cfg.openApiToken) {
|
|
187
94
|
const url = `${this.baseUrl}/devices/${id}`;
|
|
188
95
|
const opts = { headers: { Authorization: this.cfg.openApiToken } };
|
|
189
|
-
const resp = await this.fetchWithTimeoutAndRetry(url, opts
|
|
96
|
+
const resp = await this.fetchWithTimeoutAndRetry(url, opts);
|
|
190
97
|
return resp.json();
|
|
191
98
|
}
|
|
192
99
|
throw new Error('No SwitchBot client available');
|
|
193
100
|
}
|
|
101
|
+
async getDevices() {
|
|
102
|
+
// Try client API first
|
|
103
|
+
if (this.client?.getDevices) {
|
|
104
|
+
try {
|
|
105
|
+
return await this.client.getDevices();
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
this.logger?.warn?.('Client getDevices failed, trying OpenAPI fallback:', e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Fallback: call OpenAPI
|
|
112
|
+
if (this.cfg.openApiToken) {
|
|
113
|
+
const url = `${this.baseUrl}/devices`;
|
|
114
|
+
const opts = { headers: { Authorization: this.cfg.openApiToken } };
|
|
115
|
+
const resp = await this.fetchWithTimeoutAndRetry(url, opts);
|
|
116
|
+
const data = await resp.json();
|
|
117
|
+
return (data?.body || data);
|
|
118
|
+
}
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
194
121
|
async setDeviceState(id, body) {
|
|
195
|
-
//
|
|
122
|
+
// Plugin-level debounce: coalesce rapid writes per device
|
|
196
123
|
if (!this.writeDebounceMs || this.writeDebounceMs <= 0) {
|
|
197
124
|
return this._doSetDeviceState(id, body);
|
|
198
125
|
}
|
|
199
|
-
// Coalesce writes per-device within debounce window. Last write wins.
|
|
200
126
|
return new Promise((resolve, reject) => {
|
|
201
127
|
const existing = this.pendingWrites.get(id);
|
|
202
128
|
if (existing) {
|
|
203
129
|
existing.body = body;
|
|
204
130
|
existing.resolvers.push({ resolve, reject });
|
|
205
|
-
// timer already scheduled
|
|
206
131
|
return;
|
|
207
132
|
}
|
|
208
133
|
const resolvers = [{ resolve, reject }];
|
|
@@ -225,13 +150,23 @@ export class SwitchBotClient {
|
|
|
225
150
|
});
|
|
226
151
|
}
|
|
227
152
|
async _doSetDeviceState(id, body) {
|
|
228
|
-
// Prefer client API
|
|
229
|
-
if (this.client
|
|
230
|
-
|
|
153
|
+
// Prefer client API (which has node-switchbot's native resilience)
|
|
154
|
+
if (this.client?.setDeviceState) {
|
|
155
|
+
try {
|
|
156
|
+
return await this.client.setDeviceState(id, body);
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
this.logger?.warn?.(`Client setDeviceState failed for ${id}, trying fallback:`, e);
|
|
160
|
+
}
|
|
231
161
|
}
|
|
232
|
-
// Try generic
|
|
233
|
-
if (this.client
|
|
234
|
-
|
|
162
|
+
// Try generic sendCommand if available
|
|
163
|
+
if (this.client?.sendCommand) {
|
|
164
|
+
try {
|
|
165
|
+
return await this.client.sendCommand(id, body);
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
this.logger?.warn?.(`Client sendCommand failed for ${id}, trying OpenAPI:`, e);
|
|
169
|
+
}
|
|
235
170
|
}
|
|
236
171
|
// OpenAPI fallback
|
|
237
172
|
if (this.cfg.openApiToken) {
|
|
@@ -244,13 +179,13 @@ export class SwitchBotClient {
|
|
|
244
179
|
},
|
|
245
180
|
body: JSON.stringify(body),
|
|
246
181
|
};
|
|
247
|
-
const resp = await this.fetchWithTimeoutAndRetry(url, opts
|
|
182
|
+
const resp = await this.fetchWithTimeoutAndRetry(url, opts);
|
|
248
183
|
return resp.json();
|
|
249
184
|
}
|
|
250
185
|
throw new Error('No SwitchBot client available for setDeviceState');
|
|
251
186
|
}
|
|
252
187
|
async destroy() {
|
|
253
|
-
if (this.client
|
|
188
|
+
if (this.client?.destroy) {
|
|
254
189
|
await this.client.destroy();
|
|
255
190
|
}
|
|
256
191
|
this.client = null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"switchbotClient.js","sourceRoot":"","sources":["../src/switchbotClient.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"switchbotClient.js","sourceRoot":"","sources":["../src/switchbotClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE html><html class="default" lang="en" data-base="../"><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>default | @switchbot/homebridge-switchbot</title><meta name="description" content="Documentation for @switchbot/homebridge-switchbot"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script defer src="../assets/main.js"></script><script async src="../assets/icons.js" id="tsd-icons-script"></script><script async src="../assets/search.js" id="tsd-search-script"></script><script async src="../assets/navigation.js" id="tsd-nav-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => window.app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><a href="../index.html" class="title">@switchbot/homebridge-switchbot</a><div id="tsd-toolbar-links"></div><button id="tsd-search-trigger" class="tsd-widget" aria-label="Search"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-search"></use></svg></button><dialog id="tsd-search" aria-label="Search"><input role="combobox" id="tsd-search-input" aria-controls="tsd-search-results" aria-autocomplete="list" aria-expanded="true" autocapitalize="off" autocomplete="off" placeholder="Search the docs" maxLength="100"/><ul role="listbox" id="tsd-search-results"></ul><div id="tsd-search-status" aria-live="polite" aria-atomic="true"><div>Preparing search index...</div></div></dialog><a href="#" class="tsd-widget menu" id="tsd-toolbar-menu-trigger" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-menu"></use></svg></a></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb" aria-label="Breadcrumb"><li><a href="" aria-current="page">default</a></li></ul><h1>Variable default</h1></div><div class="tsd-signature"><span class="tsd-kind-variable">default</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol">(</span><span class="tsd-kind-parameter">api</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">API</span><span class="tsd-signature-symbol">)</span> <span class="tsd-signature-symbol">=></span> <span class="tsd-signature-type">void</span></div><div class="tsd-type-declaration"><h4>Type Declaration</h4><ul class="tsd-parameters"><li class="tsd-parameter-signature"><ul class="tsd-signatures"><li class="tsd-signature" id="__type"><span class="tsd-signature-symbol">(</span><span class="tsd-kind-parameter">api</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">API</span><span class="tsd-signature-symbol">)</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">void</span></li><li class="tsd-description"><div class="tsd-parameters"><h4 class="tsd-parameters-title">Parameters</h4><ul class="tsd-parameter-list"><li><span><span class="tsd-kind-parameter">api</span>: <span class="tsd-signature-type">API</span></span></li></ul></div><h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">void</span></h4></li></ul></li></ul></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/OpenWonderLabs/homebridge-switchbot/blob/
|
|
1
|
+
<!DOCTYPE html><html class="default" lang="en" data-base="../"><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>default | @switchbot/homebridge-switchbot</title><meta name="description" content="Documentation for @switchbot/homebridge-switchbot"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script defer src="../assets/main.js"></script><script async src="../assets/icons.js" id="tsd-icons-script"></script><script async src="../assets/search.js" id="tsd-search-script"></script><script async src="../assets/navigation.js" id="tsd-nav-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => window.app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><a href="../index.html" class="title">@switchbot/homebridge-switchbot</a><div id="tsd-toolbar-links"></div><button id="tsd-search-trigger" class="tsd-widget" aria-label="Search"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-search"></use></svg></button><dialog id="tsd-search" aria-label="Search"><input role="combobox" id="tsd-search-input" aria-controls="tsd-search-results" aria-autocomplete="list" aria-expanded="true" autocapitalize="off" autocomplete="off" placeholder="Search the docs" maxLength="100"/><ul role="listbox" id="tsd-search-results"></ul><div id="tsd-search-status" aria-live="polite" aria-atomic="true"><div>Preparing search index...</div></div></dialog><a href="#" class="tsd-widget menu" id="tsd-toolbar-menu-trigger" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-menu"></use></svg></a></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb" aria-label="Breadcrumb"><li><a href="" aria-current="page">default</a></li></ul><h1>Variable default</h1></div><div class="tsd-signature"><span class="tsd-kind-variable">default</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol">(</span><span class="tsd-kind-parameter">api</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">API</span><span class="tsd-signature-symbol">)</span> <span class="tsd-signature-symbol">=></span> <span class="tsd-signature-type">void</span></div><div class="tsd-type-declaration"><h4>Type Declaration</h4><ul class="tsd-parameters"><li class="tsd-parameter-signature"><ul class="tsd-signatures"><li class="tsd-signature" id="__type"><span class="tsd-signature-symbol">(</span><span class="tsd-kind-parameter">api</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">API</span><span class="tsd-signature-symbol">)</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">void</span></li><li class="tsd-description"><div class="tsd-parameters"><h4 class="tsd-parameters-title">Parameters</h4><ul class="tsd-parameter-list"><li><span><span class="tsd-kind-parameter">api</span>: <span class="tsd-signature-type">API</span></span></li></ul></div><h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">void</span></h4></li></ul></li></ul></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/OpenWonderLabs/homebridge-switchbot/blob/0c3bba22e06039260e02162e71169960ccffbc82/src/index.ts#L12">index.ts:12</a></li></ul></aside></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronDown"></use></svg><h3>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="../modules.html">@switchbot/homebridge-switchbot</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer></footer><div class="overlay"></div></body></html>
|
package/eslint.config.js
CHANGED
|
@@ -10,7 +10,6 @@ export default antfu(
|
|
|
10
10
|
},
|
|
11
11
|
rules: {
|
|
12
12
|
'curly': ['error', 'multi-line'],
|
|
13
|
-
'import/order': 0,
|
|
14
13
|
'jsdoc/check-alignment': 'error',
|
|
15
14
|
'jsdoc/check-line-alignment': 'error',
|
|
16
15
|
'perfectionist/sort-exports': 'error',
|
|
@@ -18,15 +17,10 @@ export default antfu(
|
|
|
18
17
|
'error',
|
|
19
18
|
{
|
|
20
19
|
groups: [
|
|
21
|
-
'
|
|
22
|
-
'external
|
|
23
|
-
'internal-type',
|
|
24
|
-
['parent-type', 'sibling-type', 'index-type'],
|
|
25
|
-
'builtin',
|
|
26
|
-
'external',
|
|
20
|
+
'type',
|
|
21
|
+
['builtin', 'external'],
|
|
27
22
|
'internal',
|
|
28
23
|
['parent', 'sibling', 'index'],
|
|
29
|
-
'object',
|
|
30
24
|
'unknown',
|
|
31
25
|
],
|
|
32
26
|
order: 'asc',
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@switchbot/homebridge-switchbot",
|
|
3
3
|
"displayName": "SwitchBot",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "5.0.0-beta.
|
|
5
|
+
"version": "5.0.0-beta.76",
|
|
6
6
|
"description": "The SwitchBot plugin allows you to access your SwitchBot device(s) from HomeKit.",
|
|
7
7
|
"author": "SwitchBot <support@wondertechlabs.com> (https://github.com/SwitchBot)",
|
|
8
8
|
"contributors": [
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"ir"
|
|
53
53
|
],
|
|
54
54
|
"main": "dist/index.js",
|
|
55
|
+
"bin": {},
|
|
55
56
|
"icon": "https://raw.githubusercontent.com/OpenWonderLabs/homebridge-switchbot/latest/branding/icon.png",
|
|
56
57
|
"engineStrict": true,
|
|
57
58
|
"engines": {
|
|
@@ -77,36 +78,27 @@
|
|
|
77
78
|
"docs": "typedoc",
|
|
78
79
|
"docs:lint": "typedoc --emit none --treatWarningsAsErrors"
|
|
79
80
|
},
|
|
80
|
-
"bin": {},
|
|
81
81
|
"publishConfig": {
|
|
82
82
|
"access": "public"
|
|
83
83
|
},
|
|
84
84
|
"dependencies": {
|
|
85
|
-
"@homebridge/plugin-ui-utils": "^2.2.
|
|
86
|
-
"
|
|
87
|
-
"fakegato-history": "^0.6.7",
|
|
88
|
-
"homebridge-lib": "^7.3.1",
|
|
89
|
-
"node-switchbot": "^4.0.0-beta.1",
|
|
90
|
-
"rxjs": "^7.8.2"
|
|
85
|
+
"@homebridge/plugin-ui-utils": "^2.2.1",
|
|
86
|
+
"node-switchbot": "^4.0.0-beta.2"
|
|
91
87
|
},
|
|
92
88
|
"devDependencies": {
|
|
93
|
-
"@antfu/eslint-config": "^6.
|
|
94
|
-
"@types/aes-js": "^3.1.4",
|
|
89
|
+
"@antfu/eslint-config": "^7.6.1",
|
|
95
90
|
"@types/debug": "^4.1.12",
|
|
96
91
|
"@types/fs-extra": "^11.0.4",
|
|
97
|
-
"@types/
|
|
98
|
-
"@types/node": "^24.11.0",
|
|
92
|
+
"@types/node": "^25.3.3",
|
|
99
93
|
"@types/semver": "^7.7.1",
|
|
100
94
|
"@types/source-map-support": "^0.5.10",
|
|
101
95
|
"@vitest/coverage-v8": "^4.0.18",
|
|
102
|
-
"eslint": "^
|
|
103
|
-
"eslint-plugin-format": "^
|
|
104
|
-
"eslint-plugin-import": "^2.32.0",
|
|
96
|
+
"eslint": "^10.0.2",
|
|
97
|
+
"eslint-plugin-format": "^2.0.1",
|
|
105
98
|
"homebridge": "^2.0.0-beta.76",
|
|
106
|
-
"homebridge-config-ui-x": "^5.
|
|
99
|
+
"homebridge-config-ui-x": "^5.19.0",
|
|
107
100
|
"nodemon": "^3.1.14",
|
|
108
101
|
"shx": "^0.4.0",
|
|
109
|
-
"ts-node": "^10.9.2",
|
|
110
102
|
"typedoc": "^0.28.17",
|
|
111
103
|
"typescript": "^5.9.3",
|
|
112
104
|
"vitest": "^4.0.18"
|
|
@@ -59,11 +59,11 @@
|
|
|
59
59
|
async function loadCredentialStatus() {
|
|
60
60
|
try {
|
|
61
61
|
const resp = await homebridge.request('/credentials', {})
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
console.log('Load credentials response:', resp)
|
|
63
|
+
|
|
64
|
+
if (!resp || resp.success === false) {
|
|
65
|
+
console.error('Failed to load credentials:', resp)
|
|
66
|
+
return
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const creds = resp.data || {}
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
tokenStatus.classList.add('ok')
|
|
76
76
|
} else {
|
|
77
77
|
tokenStatus.textContent = 'Not configured'
|
|
78
|
+
tokenStatus.classList.remove('ok')
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
if (creds.hasSecret) {
|
|
@@ -82,6 +83,7 @@
|
|
|
82
83
|
secretStatus.classList.add('ok')
|
|
83
84
|
} else {
|
|
84
85
|
secretStatus.textContent = 'Not configured'
|
|
86
|
+
secretStatus.classList.remove('ok')
|
|
85
87
|
}
|
|
86
88
|
} catch (e) {
|
|
87
89
|
console.error('Error loading credentials:', e)
|
|
@@ -104,12 +106,16 @@
|
|
|
104
106
|
saveBtn.disabled = true
|
|
105
107
|
saveBtn.textContent = 'Saving...'
|
|
106
108
|
|
|
109
|
+
console.log('Saving credentials...')
|
|
107
110
|
const resp = await homebridge.request('/credentials', { token, secret })
|
|
111
|
+
console.log('Save response:', resp)
|
|
112
|
+
|
|
108
113
|
if (!resp || resp.success === false) {
|
|
109
|
-
throw new Error(resp?.
|
|
114
|
+
throw new Error(resp?.message || 'Save failed')
|
|
110
115
|
}
|
|
111
116
|
|
|
112
|
-
|
|
117
|
+
const result = resp.data || resp
|
|
118
|
+
saveStatus.textContent = '✓ ' + (result.message || 'Credentials saved successfully')
|
|
113
119
|
saveStatus.classList.remove('error')
|
|
114
120
|
saveStatus.classList.add('success-msg')
|
|
115
121
|
|
|
@@ -117,8 +123,8 @@
|
|
|
117
123
|
document.getElementById('token').value = ''
|
|
118
124
|
document.getElementById('secret').value = ''
|
|
119
125
|
|
|
120
|
-
// Reload status
|
|
121
|
-
setTimeout(() => loadCredentialStatus(),
|
|
126
|
+
// Reload status to verify save
|
|
127
|
+
setTimeout(() => loadCredentialStatus(), 500)
|
|
122
128
|
|
|
123
129
|
// Clear status message
|
|
124
130
|
setTimeout(() => {
|
|
@@ -126,6 +132,7 @@
|
|
|
126
132
|
saveStatus.classList.remove('success-msg')
|
|
127
133
|
}, 3000)
|
|
128
134
|
} catch (e) {
|
|
135
|
+
console.error('Save error:', e)
|
|
129
136
|
saveStatus.textContent = 'Error: ' + (e?.message || 'Failed to save')
|
|
130
137
|
saveStatus.classList.add('error')
|
|
131
138
|
} finally {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/* eslint-disable no-console */
|
|
3
2
|
import { HomebridgePluginUiServer, RequestError } from '@homebridge/plugin-ui-utils'
|
|
3
|
+
import fs from 'node:fs/promises'
|
|
4
4
|
|
|
5
5
|
const server = new HomebridgePluginUiServer()
|
|
6
6
|
|
|
@@ -80,12 +80,15 @@ server.onRequest('/credentials', async (body: any) => {
|
|
|
80
80
|
// GET request - return current status
|
|
81
81
|
const { platform } = await getSwitchBotPlatformConfig()
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
const status = {
|
|
84
84
|
hasToken: !!platform.openApiToken,
|
|
85
85
|
hasSecret: !!platform.openApiSecret,
|
|
86
86
|
tokenLength: platform.openApiToken ? String(platform.openApiToken).length : 0,
|
|
87
87
|
secretLength: platform.openApiSecret ? String(platform.openApiSecret).length : 0,
|
|
88
88
|
}
|
|
89
|
+
|
|
90
|
+
console.log('[SwitchBot UI] GET /credentials - Status:', status)
|
|
91
|
+
return status
|
|
89
92
|
} else {
|
|
90
93
|
// POST request - save credentials
|
|
91
94
|
const { token, secret } = body
|
|
@@ -96,6 +99,10 @@ server.onRequest('/credentials', async (body: any) => {
|
|
|
96
99
|
|
|
97
100
|
const { config, platform, cfgPath } = await getSwitchBotPlatformConfig()
|
|
98
101
|
|
|
102
|
+
console.log('[SwitchBot UI] POST /credentials - Saving to platform:', platform.platform || platform.name)
|
|
103
|
+
console.log('[SwitchBot UI] Config path:', cfgPath)
|
|
104
|
+
console.log('[SwitchBot UI] Token length:', token.length, 'Secret length:', secret.length)
|
|
105
|
+
|
|
99
106
|
// Save token and secret directly on the platform config
|
|
100
107
|
platform.openApiToken = token
|
|
101
108
|
platform.openApiSecret = secret
|
|
@@ -103,12 +110,15 @@ server.onRequest('/credentials', async (body: any) => {
|
|
|
103
110
|
// Write back to config file
|
|
104
111
|
await fs.writeFile(cfgPath, JSON.stringify(config, null, 2), 'utf8')
|
|
105
112
|
|
|
113
|
+
console.log('[SwitchBot UI] Credentials saved successfully')
|
|
114
|
+
|
|
106
115
|
return {
|
|
107
116
|
success: true,
|
|
108
117
|
message: 'Credentials saved successfully',
|
|
109
118
|
}
|
|
110
119
|
}
|
|
111
120
|
} catch (e) {
|
|
121
|
+
console.error('[SwitchBot UI] Error in /credentials:', e)
|
|
112
122
|
throw new RequestError('Failed to handle credentials request', e)
|
|
113
123
|
}
|
|
114
124
|
})
|
package/src/switchbotClient.ts
CHANGED
|
@@ -3,211 +3,139 @@ import type { SwitchBotPluginConfig } from './settings.js'
|
|
|
3
3
|
export interface ISwitchBotClient {
|
|
4
4
|
init(): Promise<void>
|
|
5
5
|
getDevice(id: string): Promise<any>
|
|
6
|
+
getDevices(): Promise<any[]>
|
|
7
|
+
setDeviceState(id: string, body: any): Promise<any>
|
|
6
8
|
destroy(): Promise<void>
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Thin wrapper around node-switchbot v4.0.0-beta.2+
|
|
13
|
+
* Leverages upstream resilience features (retry, circuit breaker, connection intelligence)
|
|
14
|
+
* while maintaining plugin-specific features like write debouncing and OpenAPI fallback.
|
|
15
|
+
*/
|
|
10
16
|
export class SwitchBotClient implements ISwitchBotClient {
|
|
11
17
|
private cfg: SwitchBotPluginConfig
|
|
12
18
|
private client: any | null = null
|
|
13
19
|
private baseUrl = 'https://api.switch-bot.com/v1.0'
|
|
14
|
-
// configurable network options
|
|
15
20
|
private requestTimeout = 5000
|
|
16
21
|
private maxRetries = 2
|
|
17
|
-
private baseBackoffMs = 100
|
|
18
|
-
private maxBackoffMs = 2000
|
|
19
|
-
private backoffFactor = 2
|
|
20
|
-
private jitter = true
|
|
21
|
-
// per-device write debounce (ms). 0 = disabled. Configurable via plugin options.
|
|
22
|
-
// Default enabled to coalesce rapid writes and prevent command floods.
|
|
23
22
|
private writeDebounceMs = 100
|
|
24
|
-
// structured logger (expects info/warn/error/debug); defaults to console
|
|
25
23
|
private logger: any
|
|
26
|
-
|
|
27
|
-
private perDeviceRetries: Map<string, number> = new Map()
|
|
28
|
-
private perDeviceMaxRetries = 3
|
|
29
|
-
// per-device cooldown after exceeding retries (ms)
|
|
30
|
-
private perDeviceCooldownMs = 60_000
|
|
31
|
-
private perDeviceCooldownUntil: Map<string, number> = new Map()
|
|
24
|
+
private pendingWrites: Map<string, { timer: any; body: any; resolvers: Array<{ resolve: (v:any)=>void; reject: (e:any)=>void }>; }> = new Map()
|
|
32
25
|
|
|
33
26
|
constructor(cfg: SwitchBotPluginConfig) {
|
|
34
27
|
this.cfg = cfg
|
|
35
28
|
this.logger = (cfg as any)?.logger ?? console
|
|
36
29
|
if (typeof (cfg as any)?.requestTimeout === 'number') this.requestTimeout = (cfg as any).requestTimeout
|
|
37
30
|
if (typeof (cfg as any)?.maxRetries === 'number') this.maxRetries = (cfg as any).maxRetries
|
|
38
|
-
if (typeof (cfg as any)?.perDeviceMaxRetries === 'number') this.perDeviceMaxRetries = (cfg as any).perDeviceMaxRetries
|
|
39
|
-
if (typeof (cfg as any)?.baseBackoffMs === 'number') this.baseBackoffMs = (cfg as any).baseBackoffMs
|
|
40
|
-
if (typeof (cfg as any)?.maxBackoffMs === 'number') this.maxBackoffMs = (cfg as any).maxBackoffMs
|
|
41
|
-
if (typeof (cfg as any)?.backoffFactor === 'number') this.backoffFactor = (cfg as any).backoffFactor
|
|
42
|
-
if (typeof (cfg as any)?.jitter === 'boolean') this.jitter = (cfg as any).jitter
|
|
43
|
-
if (typeof (cfg as any)?.perDeviceCooldownMs === 'number') this.perDeviceCooldownMs = (cfg as any).perDeviceCooldownMs
|
|
44
31
|
if (typeof (cfg as any)?.writeDebounceMs === 'number') this.writeDebounceMs = (cfg as any).writeDebounceMs
|
|
45
32
|
}
|
|
46
33
|
|
|
47
|
-
// pending writes coalescing: deviceId -> { timer, body, resolvers }
|
|
48
|
-
private pendingWrites: Map<string, { timer: any; body: any; resolvers: Array<{ resolve: (v:any)=>void; reject: (e:any)=>void }>; }> = new Map()
|
|
49
|
-
|
|
50
34
|
async init(): Promise<void> {
|
|
51
|
-
// Try to dynamically import the new node-switchbot package. If it exposes
|
|
52
|
-
// a hybrid client we will use it. Otherwise the consumer can implement
|
|
53
|
-
// OpenAPI fallback logic here.
|
|
54
35
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
36
|
+
// Dynamic import of node-switchbot v4 with native resilience features
|
|
37
|
+
const { SwitchBot } = await import('node-switchbot')
|
|
38
|
+
this.client = new SwitchBot({
|
|
39
|
+
token: this.cfg.openApiToken,
|
|
40
|
+
secret: this.cfg.openApiSecret,
|
|
41
|
+
// Enable all 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
|
+
enableMetrics: true, // Connection tracking and statistics
|
|
46
|
+
...(typeof (this.cfg as any)?.nodeClientConfig === 'object' && (this.cfg as any).nodeClientConfig),
|
|
47
|
+
})
|
|
48
|
+
this.logger?.info?.('SwitchBot client initialized with native resilience features')
|
|
63
49
|
} catch (e) {
|
|
64
|
-
|
|
65
|
-
this.logger?.warn?.('node-switchbot import failed; falling back to OpenAPI when token available')
|
|
50
|
+
this.logger?.warn?.('Failed to load node-switchbot; will use OpenAPI fallback:', e)
|
|
66
51
|
this.client = null
|
|
67
52
|
}
|
|
68
53
|
}
|
|
69
54
|
|
|
70
|
-
private async fetchWithTimeoutAndRetry(url: string, opts: any = {}, timeoutMs?: number, retries?: number
|
|
55
|
+
private async fetchWithTimeoutAndRetry(url: string, opts: any = {}, timeoutMs?: number, retries?: number) {
|
|
71
56
|
const to = typeof timeoutMs === 'number' ? timeoutMs : this.requestTimeout
|
|
72
57
|
const max = typeof retries === 'number' ? retries : this.maxRetries
|
|
73
58
|
|
|
74
|
-
// Check per-device cooldown
|
|
75
|
-
if (deviceId) {
|
|
76
|
-
const until = this.perDeviceCooldownUntil.get(deviceId) ?? 0
|
|
77
|
-
if (Date.now() < until) {
|
|
78
|
-
const ms = until - Date.now()
|
|
79
|
-
this.logger?.warn?.('device in cooldown, aborting fetch', { deviceId, cooldownMs: ms })
|
|
80
|
-
throw new Error(`device ${deviceId} in cooldown for ${ms}ms`)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
59
|
let attempt = 0
|
|
85
60
|
while (true) {
|
|
86
61
|
attempt += 1
|
|
87
|
-
// AbortController-based timeout
|
|
88
62
|
const controller = new AbortController()
|
|
89
63
|
const timer = setTimeout(() => controller.abort(), to)
|
|
90
64
|
try {
|
|
91
|
-
this.logger?.debug?.('fetch', { url, attempt, deviceId })
|
|
92
65
|
const fetchOpts = Object.assign({}, opts)
|
|
93
66
|
try {
|
|
94
67
|
Object.defineProperty(fetchOpts, 'signal', { value: controller.signal, enumerable: false, configurable: true })
|
|
95
68
|
} catch (_e) {
|
|
96
|
-
// ignore
|
|
69
|
+
// ignore
|
|
97
70
|
}
|
|
98
71
|
const resp = await fetch(url, fetchOpts)
|
|
99
72
|
clearTimeout(timer)
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (!r) return ''
|
|
105
|
-
if (typeof r.text === 'function') return await r.text()
|
|
106
|
-
if (typeof r.json === 'function') {
|
|
107
|
-
const j = await r.json().catch(() => null)
|
|
108
|
-
return j ? JSON.stringify(j) : ''
|
|
109
|
-
}
|
|
110
|
-
return String(r)
|
|
111
|
-
} catch (_e) {
|
|
112
|
-
return ''
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Some tests/mocks provide response-like objects without numeric `status`.
|
|
117
|
-
// In those cases attempt to parse the body and return a Response-like
|
|
118
|
-
// object so callers (resp.json()) continue to work instead of forcing
|
|
119
|
-
// a retry path.
|
|
120
|
-
if (typeof (resp as any).status !== 'number') {
|
|
121
|
-
const bodyStr = await safeReadBody(resp)
|
|
122
|
-
try {
|
|
123
|
-
const parsed = bodyStr ? JSON.parse(bodyStr) : {}
|
|
124
|
-
if (deviceId) this.perDeviceRetries.set(deviceId, 0)
|
|
125
|
-
return { ok: true, status: 200, json: async () => parsed }
|
|
126
|
-
} catch (_e) {
|
|
127
|
-
if (deviceId) this.perDeviceRetries.set(deviceId, 0)
|
|
128
|
-
return { ok: true, status: 200, json: async () => ({ body: bodyStr }) }
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// classify response
|
|
133
|
-
if (resp.ok) {
|
|
134
|
-
if (deviceId) this.perDeviceRetries.set(deviceId, 0)
|
|
135
|
-
return resp
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Do not retry on client errors (4xx), except 429 (rate limit)
|
|
139
|
-
if (resp.status >= 400 && resp.status < 500 && resp.status !== 429) {
|
|
140
|
-
const body = await safeReadBody(resp)
|
|
141
|
-
this.logger?.error?.('fetch non-retriable response', { url, status: resp.status, body })
|
|
142
|
-
const err = new Error(`HTTP ${resp.status}`)
|
|
143
|
-
;(err as any).status = resp.status
|
|
144
|
-
throw err
|
|
73
|
+
if (resp.ok) return resp
|
|
74
|
+
// server error / rate limit: retry
|
|
75
|
+
if (resp.status >= 500 || resp.status === 429) {
|
|
76
|
+
throw new Error(`retriable response: ${resp.status}`)
|
|
145
77
|
}
|
|
146
|
-
|
|
147
|
-
// server error / rate limit: treat as retriable
|
|
148
|
-
const body = await safeReadBody(resp)
|
|
149
|
-
this.logger?.warn?.('fetch retriable response', { url, status: resp.status, body })
|
|
150
|
-
// throw to enter retry/catch logic below and apply backoff
|
|
151
|
-
throw new Error(`retriable response: ${resp.status || 'unknown'}`)
|
|
78
|
+
return resp
|
|
152
79
|
} catch (err: any) {
|
|
153
80
|
clearTimeout(timer)
|
|
154
|
-
const isAbort = err?.name === 'AbortError' || err?.message === 'The user aborted a request.' || err?.message === 'timeout'
|
|
155
|
-
const reason = isAbort ? 'timeout' : err?.message ?? String(err)
|
|
156
|
-
this.logger?.warn?.('fetch failed', { url, attempt, deviceId, reason })
|
|
157
|
-
|
|
158
|
-
// per-device retry guard
|
|
159
|
-
if (deviceId) {
|
|
160
|
-
const prev = this.perDeviceRetries.get(deviceId) ?? 0
|
|
161
|
-
const next = prev + 1
|
|
162
|
-
this.perDeviceRetries.set(deviceId, next)
|
|
163
|
-
if (next > this.perDeviceMaxRetries) {
|
|
164
|
-
this.logger?.error?.('per-device retry limit exceeded, entering cooldown', { deviceId, attempts: next })
|
|
165
|
-
this.perDeviceCooldownUntil.set(deviceId, Date.now() + this.perDeviceCooldownMs)
|
|
166
|
-
throw new Error(`per-device retry limit exceeded for ${deviceId}`)
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
81
|
if (attempt > max) throw err
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
let backoff = Math.min(this.baseBackoffMs * Math.pow(this.backoffFactor, attempt - 1), this.maxBackoffMs)
|
|
174
|
-
if (this.jitter) {
|
|
175
|
-
const jitterVal = Math.floor(Math.random() * backoff)
|
|
176
|
-
backoff = Math.floor(backoff / 2) + jitterVal
|
|
177
|
-
}
|
|
178
|
-
this.logger?.debug?.('backoff before retry', { url, attempt, backoff })
|
|
82
|
+
// exponential backoff
|
|
83
|
+
const backoff = Math.min(100 * Math.pow(2, attempt - 1), 2000)
|
|
179
84
|
await new Promise((r) => setTimeout(r, backoff))
|
|
180
85
|
}
|
|
181
86
|
}
|
|
182
87
|
}
|
|
183
88
|
|
|
184
89
|
async getDevice(id: string): Promise<any> {
|
|
185
|
-
|
|
186
|
-
|
|
90
|
+
// Try client API first (with node-switchbot's smart fallback and retry)
|
|
91
|
+
if (this.client?.getDevice) {
|
|
92
|
+
try {
|
|
93
|
+
return await this.client.getDevice(id)
|
|
94
|
+
} catch (e) {
|
|
95
|
+
this.logger?.warn?.(`Client getDevice failed for ${id}, trying OpenAPI fallback:`, e)
|
|
96
|
+
}
|
|
187
97
|
}
|
|
188
|
-
// Fallback: call OpenAPI via HTTP
|
|
98
|
+
// Fallback: call OpenAPI via HTTP
|
|
189
99
|
if (this.cfg.openApiToken) {
|
|
190
100
|
const url = `${this.baseUrl}/devices/${id}`
|
|
191
101
|
const opts = { headers: { Authorization: this.cfg.openApiToken } }
|
|
192
|
-
const resp = await this.fetchWithTimeoutAndRetry(url, opts
|
|
102
|
+
const resp = await this.fetchWithTimeoutAndRetry(url, opts)
|
|
193
103
|
return resp.json()
|
|
194
104
|
}
|
|
195
105
|
throw new Error('No SwitchBot client available')
|
|
196
106
|
}
|
|
197
107
|
|
|
108
|
+
async getDevices(): Promise<any[]> {
|
|
109
|
+
// Try client API first
|
|
110
|
+
if (this.client?.getDevices) {
|
|
111
|
+
try {
|
|
112
|
+
return await this.client.getDevices()
|
|
113
|
+
} catch (e) {
|
|
114
|
+
this.logger?.warn?.('Client getDevices failed, trying OpenAPI fallback:', e)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Fallback: call OpenAPI
|
|
118
|
+
if (this.cfg.openApiToken) {
|
|
119
|
+
const url = `${this.baseUrl}/devices`
|
|
120
|
+
const opts = { headers: { Authorization: this.cfg.openApiToken } }
|
|
121
|
+
const resp = await this.fetchWithTimeoutAndRetry(url, opts)
|
|
122
|
+
const data = await resp.json()
|
|
123
|
+
return (data?.body || data) as any[]
|
|
124
|
+
}
|
|
125
|
+
return []
|
|
126
|
+
}
|
|
127
|
+
|
|
198
128
|
async setDeviceState(id: string, body: any): Promise<any> {
|
|
199
|
-
//
|
|
129
|
+
// Plugin-level debounce: coalesce rapid writes per device
|
|
200
130
|
if (!this.writeDebounceMs || this.writeDebounceMs <= 0) {
|
|
201
131
|
return this._doSetDeviceState(id, body)
|
|
202
132
|
}
|
|
203
133
|
|
|
204
|
-
// Coalesce writes per-device within debounce window. Last write wins.
|
|
205
134
|
return new Promise((resolve, reject) => {
|
|
206
135
|
const existing = this.pendingWrites.get(id)
|
|
207
136
|
if (existing) {
|
|
208
137
|
existing.body = body
|
|
209
138
|
existing.resolvers.push({ resolve, reject })
|
|
210
|
-
// timer already scheduled
|
|
211
139
|
return
|
|
212
140
|
}
|
|
213
141
|
|
|
@@ -229,14 +157,22 @@ export class SwitchBotClient implements ISwitchBotClient {
|
|
|
229
157
|
}
|
|
230
158
|
|
|
231
159
|
private async _doSetDeviceState(id: string, body: any): Promise<any> {
|
|
232
|
-
// Prefer client API
|
|
233
|
-
if (this.client
|
|
234
|
-
|
|
160
|
+
// Prefer client API (which has node-switchbot's native resilience)
|
|
161
|
+
if (this.client?.setDeviceState) {
|
|
162
|
+
try {
|
|
163
|
+
return await this.client.setDeviceState(id, body)
|
|
164
|
+
} catch (e) {
|
|
165
|
+
this.logger?.warn?.(`Client setDeviceState failed for ${id}, trying fallback:`, e)
|
|
166
|
+
}
|
|
235
167
|
}
|
|
236
168
|
|
|
237
|
-
// Try generic
|
|
238
|
-
if (this.client
|
|
239
|
-
|
|
169
|
+
// Try generic sendCommand if available
|
|
170
|
+
if (this.client?.sendCommand) {
|
|
171
|
+
try {
|
|
172
|
+
return await this.client.sendCommand(id, body)
|
|
173
|
+
} catch (e) {
|
|
174
|
+
this.logger?.warn?.(`Client sendCommand failed for ${id}, trying OpenAPI:`, e)
|
|
175
|
+
}
|
|
240
176
|
}
|
|
241
177
|
|
|
242
178
|
// OpenAPI fallback
|
|
@@ -250,7 +186,7 @@ export class SwitchBotClient implements ISwitchBotClient {
|
|
|
250
186
|
},
|
|
251
187
|
body: JSON.stringify(body),
|
|
252
188
|
}
|
|
253
|
-
const resp = await this.fetchWithTimeoutAndRetry(url, opts
|
|
189
|
+
const resp = await this.fetchWithTimeoutAndRetry(url, opts)
|
|
254
190
|
return resp.json()
|
|
255
191
|
}
|
|
256
192
|
|
|
@@ -258,9 +194,10 @@ export class SwitchBotClient implements ISwitchBotClient {
|
|
|
258
194
|
}
|
|
259
195
|
|
|
260
196
|
async destroy(): Promise<void> {
|
|
261
|
-
if (this.client
|
|
197
|
+
if (this.client?.destroy) {
|
|
262
198
|
await this.client.destroy()
|
|
263
199
|
}
|
|
264
200
|
this.client = null
|
|
265
201
|
}
|
|
266
202
|
}
|
|
203
|
+
|