@switchbot/homebridge-switchbot 5.0.0-beta.98 → 5.0.0
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/.changeset/config.json +14 -0
- package/.github/copilot-instructions.md +39 -0
- package/.github/workflows/ci.yml +4 -1
- package/.github/workflows/manual-e2e.yml +6 -3
- package/.github/workflows/release.yml +64 -15
- package/.github/workflows/stale.yml +2 -4
- package/.husky/pre-push +15 -0
- package/CHANGELOG.md +126 -134
- package/MIGRATION.md +16 -6
- package/README.md +84 -3
- package/TODO.md +263 -0
- package/config.schema.json +229 -36
- package/dist/SwitchBotHAPPlatform.d.ts +133 -0
- package/dist/SwitchBotHAPPlatform.d.ts.map +1 -0
- package/dist/SwitchBotHAPPlatform.js +555 -0
- package/dist/SwitchBotHAPPlatform.js.map +1 -0
- package/dist/SwitchBotMatterPlatform.d.ts +141 -0
- package/dist/SwitchBotMatterPlatform.d.ts.map +1 -0
- package/dist/SwitchBotMatterPlatform.js +536 -0
- package/dist/SwitchBotMatterPlatform.js.map +1 -0
- package/dist/device-types.d.ts +31 -0
- package/dist/device-types.d.ts.map +1 -0
- package/dist/device-types.js +246 -0
- package/dist/device-types.js.map +1 -0
- package/dist/deviceCommandMapper.d.ts +10 -0
- package/dist/deviceCommandMapper.d.ts.map +1 -0
- package/dist/deviceCommandMapper.js +319 -0
- package/dist/deviceCommandMapper.js.map +1 -0
- package/dist/deviceFactory.d.ts +3 -2
- package/dist/deviceFactory.d.ts.map +1 -1
- package/dist/deviceFactory.js +107 -29
- package/dist/deviceFactory.js.map +1 -1
- package/dist/devices/genericDevice.d.ts +59 -37
- package/dist/devices/genericDevice.d.ts.map +1 -1
- package/dist/devices/genericDevice.js +376 -78
- package/dist/devices/genericDevice.js.map +1 -1
- package/dist/errors.d.ts +38 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +32 -0
- package/dist/errors.js.map +1 -0
- 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/config.d.ts +3 -0
- package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -0
- package/dist/homebridge-ui/endpoints/config.js +90 -0
- package/dist/homebridge-ui/endpoints/config.js.map +1 -0
- package/dist/homebridge-ui/endpoints/devices.d.ts +6 -0
- package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -0
- package/dist/homebridge-ui/endpoints/devices.js +144 -0
- package/dist/homebridge-ui/endpoints/devices.js.map +1 -0
- package/dist/homebridge-ui/endpoints/discovery.d.ts +7 -0
- package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -0
- package/dist/homebridge-ui/endpoints/discovery.js +219 -0
- package/dist/homebridge-ui/endpoints/discovery.js.map +1 -0
- 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/css/styles.css +483 -0
- package/dist/homebridge-ui/public/index.html +197 -621
- package/dist/homebridge-ui/public/js/advanced-settings.d.ts +3 -0
- package/dist/homebridge-ui/public/js/advanced-settings.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/advanced-settings.js +95 -0
- package/dist/homebridge-ui/public/js/advanced-settings.js.map +1 -0
- package/dist/homebridge-ui/public/js/advanced-settings.ts +94 -0
- package/dist/homebridge-ui/public/js/api.d.ts +66 -0
- package/dist/homebridge-ui/public/js/api.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/api.js +295 -0
- package/dist/homebridge-ui/public/js/api.js.map +1 -0
- package/dist/homebridge-ui/public/js/api.ts +355 -0
- package/dist/homebridge-ui/public/js/app.d.ts +2 -0
- package/dist/homebridge-ui/public/js/app.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/app.js +3722 -0
- package/dist/homebridge-ui/public/js/app.js.map +7 -0
- package/dist/homebridge-ui/public/js/app.ts +22 -0
- package/dist/homebridge-ui/public/js/constants.d.ts +2 -0
- package/dist/homebridge-ui/public/js/constants.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/constants.js +2 -0
- package/dist/homebridge-ui/public/js/constants.js.map +1 -0
- package/dist/homebridge-ui/public/js/constants.ts +1 -0
- package/dist/homebridge-ui/public/js/credentials.d.ts +3 -0
- package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/credentials.js +99 -0
- package/dist/homebridge-ui/public/js/credentials.js.map +1 -0
- package/dist/homebridge-ui/public/js/credentials.ts +105 -0
- package/dist/homebridge-ui/public/js/devices-delete.d.ts +3 -0
- package/dist/homebridge-ui/public/js/devices-delete.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/devices-delete.js +199 -0
- package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -0
- package/dist/homebridge-ui/public/js/devices-delete.ts +227 -0
- package/dist/homebridge-ui/public/js/devices.d.ts +9 -0
- package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/devices.js +98 -0
- package/dist/homebridge-ui/public/js/devices.js.map +1 -0
- package/dist/homebridge-ui/public/js/devices.ts +106 -0
- package/dist/homebridge-ui/public/js/discovery.d.ts +9 -0
- package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/discovery.js +1201 -0
- package/dist/homebridge-ui/public/js/discovery.js.map +1 -0
- package/dist/homebridge-ui/public/js/discovery.ts +1335 -0
- package/dist/homebridge-ui/public/js/logger.d.ts +7 -0
- package/dist/homebridge-ui/public/js/logger.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/logger.js +17 -0
- package/dist/homebridge-ui/public/js/logger.js.map +1 -0
- package/dist/homebridge-ui/public/js/logger.ts +17 -0
- package/dist/homebridge-ui/public/js/modal.d.ts +5 -0
- package/dist/homebridge-ui/public/js/modal.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/modal.js +35 -0
- package/dist/homebridge-ui/public/js/modal.js.map +1 -0
- package/dist/homebridge-ui/public/js/modal.ts +35 -0
- package/dist/homebridge-ui/public/js/modals.d.ts +15 -0
- package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/modals.js +675 -0
- package/dist/homebridge-ui/public/js/modals.js.map +1 -0
- package/dist/homebridge-ui/public/js/modals.ts +765 -0
- package/dist/homebridge-ui/public/js/render.d.ts +71 -0
- package/dist/homebridge-ui/public/js/render.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/render.js +960 -0
- package/dist/homebridge-ui/public/js/render.js.map +1 -0
- package/dist/homebridge-ui/public/js/render.ts +1084 -0
- package/dist/homebridge-ui/public/js/toast.d.ts +6 -0
- package/dist/homebridge-ui/public/js/toast.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/toast.js +38 -0
- package/dist/homebridge-ui/public/js/toast.js.map +1 -0
- package/dist/homebridge-ui/public/js/toast.ts +44 -0
- package/dist/homebridge-ui/public/js/types.d.ts +23 -0
- package/dist/homebridge-ui/public/js/types.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/types.js +2 -0
- package/dist/homebridge-ui/public/js/types.js.map +1 -0
- package/dist/homebridge-ui/public/js/types.ts +26 -0
- package/dist/homebridge-ui/server.d.ts +1 -3
- package/dist/homebridge-ui/server.d.ts.map +1 -1
- package/dist/homebridge-ui/server.js +8 -450
- package/dist/homebridge-ui/server.js.map +1 -1
- 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/homebridge-ui/utils/config-parser.d.ts +39 -0
- package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -0
- package/dist/homebridge-ui/utils/config-parser.js +108 -0
- package/dist/homebridge-ui/utils/config-parser.js.map +1 -0
- package/dist/homebridge-ui/utils/device-migration.d.ts +35 -0
- package/dist/homebridge-ui/utils/device-migration.d.ts.map +1 -0
- package/dist/homebridge-ui/utils/device-migration.js +111 -0
- package/dist/homebridge-ui/utils/device-migration.js.map +1 -0
- package/dist/homebridge-ui/utils/logger.d.ts +7 -0
- package/dist/homebridge-ui/utils/logger.d.ts.map +1 -0
- package/dist/homebridge-ui/utils/logger.js +17 -0
- package/dist/homebridge-ui/utils/logger.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/settings.d.ts +1 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +1 -0
- package/dist/settings.js.map +1 -1
- package/dist/switchbotClient.d.ts +12 -10
- package/dist/switchbotClient.d.ts.map +1 -1
- package/dist/switchbotClient.js +156 -103
- package/dist/switchbotClient.js.map +1 -1
- package/dist/utils.d.ts +76 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +1121 -4
- package/dist/utils.js.map +1 -1
- package/docs/assets/highlight.css +16 -2
- package/docs/assets/main.js +1 -1
- package/docs/index.html +82 -5
- package/docs/variables/default.html +3 -1
- package/eslint.config.js +9 -5
- package/nodemon.json +2 -2
- package/package.json +34 -21
- package/scripts/build-ui.js +37 -0
- package/scripts/free-dev-ports.mjs +105 -0
- package/scripts/generate-matter-maps.js +34 -17
- package/scripts/sync-device-types.mjs +31 -0
- package/src/SwitchBotHAPPlatform.ts +558 -0
- package/src/SwitchBotMatterPlatform.ts +538 -0
- package/src/device-types.js +246 -0
- package/src/device-types.js.map +1 -0
- package/src/device-types.ts +261 -0
- package/src/deviceCommandMapper.js +319 -0
- package/src/deviceCommandMapper.js.map +1 -0
- package/src/deviceCommandMapper.ts +333 -0
- package/src/deviceFactory.ts +125 -45
- package/src/devices/genericDevice.ts +411 -69
- package/src/errors.js +32 -0
- package/src/errors.js.map +1 -0
- package/src/errors.ts +35 -0
- package/src/homebridge-ui/endpoints/config.ts +110 -0
- package/src/homebridge-ui/endpoints/devices.ts +153 -0
- package/src/homebridge-ui/endpoints/discovery.ts +240 -0
- package/src/homebridge-ui/public/css/styles.css +483 -0
- package/src/homebridge-ui/public/index.html +197 -621
- package/src/homebridge-ui/public/js/advanced-settings.ts +94 -0
- package/src/homebridge-ui/public/js/api.ts +355 -0
- package/src/homebridge-ui/public/js/app.ts +22 -0
- package/src/homebridge-ui/public/js/constants.ts +1 -0
- package/src/homebridge-ui/public/js/credentials.ts +105 -0
- package/src/homebridge-ui/public/js/devices-delete.ts +227 -0
- package/src/homebridge-ui/public/js/devices.ts +106 -0
- package/src/homebridge-ui/public/js/discovery.ts +1335 -0
- package/src/homebridge-ui/public/js/logger.ts +17 -0
- package/src/homebridge-ui/public/js/modal.ts +35 -0
- package/src/homebridge-ui/public/js/modals.ts +765 -0
- package/src/homebridge-ui/public/js/render.ts +1084 -0
- package/src/homebridge-ui/public/js/toast.ts +44 -0
- package/src/homebridge-ui/public/js/types.ts +26 -0
- package/src/homebridge-ui/server.ts +9 -526
- package/src/homebridge-ui/utils/config-parser.ts +125 -0
- package/src/homebridge-ui/utils/device-migration.ts +144 -0
- package/src/homebridge-ui/utils/logger.ts +17 -0
- package/src/index.ts +12 -2
- package/src/settings.js +8 -0
- package/src/settings.js.map +1 -0
- package/src/settings.ts +2 -0
- package/src/switchbotClient.js +247 -0
- package/src/switchbotClient.js.map +1 -0
- package/src/switchbotClient.ts +177 -114
- package/src/utils.ts +1133 -5
- package/test/client/switchbot-client-debounce.spec.ts +35 -0
- package/test/client/switchbot-client-openapi.spec.ts +19 -0
- package/test/client/switchbotClient.spec.ts +64 -0
- package/test/device/device-mapping.spec.ts +23 -0
- package/test/device/deviceBase.spec.ts +26 -0
- package/test/device/deviceFactory-edge.spec.ts +15 -0
- package/test/device/deviceFactory.spec.ts +33 -0
- package/test/device/fan-swing.spec.ts +34 -0
- package/test/device/genericDevice-blepoll.spec.ts +47 -0
- package/test/device/irdevice.spec.ts +9 -0
- package/test/device/lock-users.spec.ts +35 -0
- package/test/device/matter-descriptors.spec.ts +22 -0
- package/test/device/matter-device-state.spec.ts +37 -0
- package/test/e2e/run-e2e.spec.ts +18 -19
- package/test/errors/errors.spec.ts +10 -0
- package/test/helpers/matter-harness.ts +20 -9
- package/test/homebridge-ui/server.spec.ts +9 -0
- package/test/platform/accessory-restore.spec.ts +37 -0
- package/test/platform/matter-childbridge.spec.ts +34 -0
- package/test/platform/matter-integration.spec.ts +33 -0
- package/test/platform/platform-edge.spec.ts +73 -0
- package/test/platform/platform.integration.spec.ts +34 -0
- package/test/utils/utils-extra.spec.ts +10 -0
- package/test/utils/utils.spec.ts +53 -0
- package/todo/TODO.md +80 -0
- package/tsconfig.ui.json +11 -0
- package/.github/npm-version-script-esm.js +0 -97
- package/.github/workflows/beta-release.yml +0 -52
- package/dist/platform.d.ts +0 -35
- package/dist/platform.d.ts.map +0 -1
- package/dist/platform.js +0 -850
- package/dist/platform.js.map +0 -1
- package/src/platform.ts +0 -867
- package/test/accessory-restore.spec.ts +0 -73
- package/test/device-mapping.spec.ts +0 -37
- package/test/deviceFactory.spec.ts +0 -18
- package/test/fan-swing.spec.ts +0 -29
- package/test/lock-users.spec.ts +0 -44
- package/test/matter-childbridge.spec.ts +0 -55
- package/test/matter-descriptors.spec.ts +0 -97
- package/test/matter-device-state.spec.ts +0 -101
- package/test/matter-integration.spec.ts +0 -70
- package/test/platform.integration.spec.ts +0 -55
- package/test/switchbot-client-debounce.spec.ts +0 -131
- package/test/switchbot-client-openapi.spec.ts +0 -56
- package/test/switchbotClient.spec.ts +0 -10
- package/test/utils.spec.ts +0 -20
package/src/switchbotClient.ts
CHANGED
|
@@ -1,50 +1,68 @@
|
|
|
1
1
|
import type { SwitchBotPluginConfig } from './settings.js'
|
|
2
|
+
import type { SwitchBot } from 'node-switchbot'
|
|
3
|
+
|
|
4
|
+
import { getDeviceCommandHandler } from './deviceCommandMapper.js'
|
|
5
|
+
import { CharacteristicMissingError, SwitchbotAuthenticationError, SwitchbotOperationError } from './errors.js'
|
|
2
6
|
|
|
3
7
|
export interface ISwitchBotClient {
|
|
4
|
-
init()
|
|
5
|
-
getDevice(id: string)
|
|
6
|
-
getDevices()
|
|
7
|
-
setDeviceState(id: string, body: any)
|
|
8
|
-
destroy()
|
|
8
|
+
init: () => Promise<void>
|
|
9
|
+
getDevice: (id: string) => Promise<any>
|
|
10
|
+
getDevices: () => Promise<any[]>
|
|
11
|
+
setDeviceState: (id: string, body: any) => Promise<any>
|
|
12
|
+
destroy: () => Promise<void>
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
|
-
* Thin wrapper around node-switchbot v4.0.0
|
|
16
|
+
* Thin wrapper around node-switchbot v4.0.0+
|
|
13
17
|
* Leverages upstream resilience features (retry, circuit breaker, connection intelligence)
|
|
14
18
|
* while maintaining plugin-specific features like write debouncing and OpenAPI fallback.
|
|
15
19
|
*/
|
|
16
20
|
export class SwitchBotClient implements ISwitchBotClient {
|
|
17
21
|
private cfg: SwitchBotPluginConfig
|
|
18
|
-
private client:
|
|
19
|
-
private baseUrl = 'https://api.switch-bot.com/v1.0'
|
|
20
|
-
private requestTimeout = 5000
|
|
21
|
-
private maxRetries = 2
|
|
22
|
+
private client: SwitchBot | null = null
|
|
22
23
|
private writeDebounceMs = 100
|
|
23
|
-
private
|
|
24
|
-
private
|
|
24
|
+
private discoveryCacheTtlMs = 30_000
|
|
25
|
+
private lastDiscoveryAt = 0
|
|
26
|
+
private logger: import('homebridge').Logger
|
|
27
|
+
private pendingWrites: Map<string, { timer: any, body: any, resolvers: Array<{ resolve: (v: any) => void, reject: (e: any) => void }> }> = new Map()
|
|
25
28
|
|
|
26
29
|
constructor(cfg: SwitchBotPluginConfig) {
|
|
27
30
|
this.cfg = cfg
|
|
28
|
-
this.logger = (cfg as any)?.logger
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
this.logger = (cfg as any)?.logger as import('homebridge').Logger
|
|
32
|
+
if (!this.logger) {
|
|
33
|
+
throw new Error('SwitchBotClient requires a logger (Homebridge logger) in config')
|
|
34
|
+
}
|
|
35
|
+
if (typeof (cfg as any)?.writeDebounceMs === 'number') {
|
|
36
|
+
this.writeDebounceMs = (cfg as any).writeDebounceMs
|
|
37
|
+
}
|
|
38
|
+
if (typeof (cfg as any)?.discoveryCacheTtlMs === 'number') {
|
|
39
|
+
this.discoveryCacheTtlMs = Math.max(0, (cfg as any).discoveryCacheTtlMs)
|
|
40
|
+
}
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
async init(): Promise<void> {
|
|
44
|
+
if (this.client) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
try {
|
|
36
49
|
// Dynamic import of node-switchbot v4 with native resilience features
|
|
37
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)
|
|
38
53
|
this.client = new SwitchBot({
|
|
39
54
|
token: this.cfg.openApiToken,
|
|
40
55
|
secret: this.cfg.openApiSecret,
|
|
41
|
-
// Enable
|
|
42
|
-
enableFallback: true,
|
|
43
|
-
enableRetry: true,
|
|
44
|
-
enableCircuitBreaker: true,
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
// Enable built-in resilience features from node-switchbot v4.
|
|
57
|
+
enableFallback: true, // Auto-fallback from BLE to API
|
|
58
|
+
enableRetry: true, // Retry with exponential backoff
|
|
59
|
+
enableCircuitBreaker: true, // Circuit breaker per connection type
|
|
60
|
+
enableConnectionIntelligence: true, // Connection tracking and route preference
|
|
61
|
+
enableBLE: this.cfg.enableBLE !== false, // Use config value, default true
|
|
62
|
+
scanTimeout,
|
|
63
|
+
...rawNodeClientConfig,
|
|
47
64
|
})
|
|
65
|
+
this.lastDiscoveryAt = 0
|
|
48
66
|
this.logger?.info?.('SwitchBot client initialized with native resilience features')
|
|
49
67
|
} catch (e) {
|
|
50
68
|
this.logger?.warn?.('Failed to load node-switchbot; will use OpenAPI fallback:', e)
|
|
@@ -52,77 +70,55 @@ export class SwitchBotClient implements ISwitchBotClient {
|
|
|
52
70
|
}
|
|
53
71
|
}
|
|
54
72
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const max = typeof retries === 'number' ? retries : this.maxRetries
|
|
58
|
-
|
|
59
|
-
let attempt = 0
|
|
60
|
-
while (true) {
|
|
61
|
-
attempt += 1
|
|
62
|
-
const controller = new AbortController()
|
|
63
|
-
const timer = setTimeout(() => controller.abort(), to)
|
|
73
|
+
async getDevice(id: string): Promise<any> {
|
|
74
|
+
if (this.client) {
|
|
64
75
|
try {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
} catch (_e) {
|
|
69
|
-
// ignore
|
|
76
|
+
const fromManager = this.getManagedDevice(id)
|
|
77
|
+
if (fromManager) {
|
|
78
|
+
return fromManager
|
|
70
79
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
throw new Error(`retriable response: ${resp.status}`)
|
|
80
|
+
|
|
81
|
+
const devices = await this.ensureDiscovered(false)
|
|
82
|
+
const fromDiscovery = devices.find((d: any) => d.id === id)
|
|
83
|
+
if (fromDiscovery) {
|
|
84
|
+
return fromDiscovery
|
|
77
85
|
}
|
|
78
|
-
return resp
|
|
79
|
-
} catch (err: any) {
|
|
80
|
-
clearTimeout(timer)
|
|
81
|
-
if (attempt > max) throw err
|
|
82
|
-
// exponential backoff
|
|
83
|
-
const backoff = Math.min(100 * Math.pow(2, attempt - 1), 2000)
|
|
84
|
-
await new Promise((r) => setTimeout(r, backoff))
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
87
|
+
const refreshDevices = await this.ensureDiscovered(true)
|
|
88
|
+
return refreshDevices.find((d: any) => d.id === id)
|
|
89
|
+
} catch (e: any) {
|
|
90
|
+
if (e instanceof SwitchbotAuthenticationError) {
|
|
91
|
+
this.logger?.error?.(`Authentication error for getDevice(${id}):`, e.message)
|
|
92
|
+
throw e
|
|
93
|
+
} else if (e instanceof SwitchbotOperationError) {
|
|
94
|
+
this.logger?.warn?.(`Operation error for getDevice(${id}):`, e.message, e.code)
|
|
95
|
+
throw e
|
|
96
|
+
} else if (e instanceof CharacteristicMissingError) {
|
|
97
|
+
this.logger?.warn?.(`Characteristic missing for getDevice(${id}):`, e.characteristic)
|
|
98
|
+
throw e
|
|
99
|
+
} else {
|
|
100
|
+
this.logger?.warn?.(`Client getDevice failed for ${id}:`, e)
|
|
101
|
+
throw e
|
|
102
|
+
}
|
|
96
103
|
}
|
|
97
104
|
}
|
|
98
|
-
|
|
99
|
-
if (this.cfg.openApiToken) {
|
|
100
|
-
const url = `${this.baseUrl}/devices/${id}`
|
|
101
|
-
const opts = { headers: { Authorization: this.cfg.openApiToken } }
|
|
102
|
-
const resp = await this.fetchWithTimeoutAndRetry(url, opts)
|
|
103
|
-
return resp.json()
|
|
104
|
-
}
|
|
105
|
-
throw new Error('No SwitchBot client available')
|
|
105
|
+
throw new SwitchbotOperationError('No SwitchBot client available', 'no_client')
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
async getDevices(): Promise<any[]> {
|
|
109
|
-
|
|
110
|
-
if (this.client?.getDevices) {
|
|
109
|
+
if (this.client) {
|
|
111
110
|
try {
|
|
112
|
-
|
|
111
|
+
const fromManager = this.getManagedDevices()
|
|
112
|
+
if (fromManager.length > 0) {
|
|
113
|
+
return fromManager
|
|
114
|
+
}
|
|
115
|
+
return await this.ensureDiscovered(false)
|
|
113
116
|
} catch (e) {
|
|
114
|
-
this.logger?.warn?.('Client getDevices failed
|
|
117
|
+
this.logger?.warn?.('Client getDevices failed:', e)
|
|
118
|
+
throw e
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
|
-
|
|
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 []
|
|
121
|
+
throw new SwitchbotOperationError('No SwitchBot client available', 'no_client')
|
|
126
122
|
}
|
|
127
123
|
|
|
128
124
|
async setDeviceState(id: string, body: any): Promise<any> {
|
|
@@ -139,15 +135,24 @@ export class SwitchBotClient implements ISwitchBotClient {
|
|
|
139
135
|
return
|
|
140
136
|
}
|
|
141
137
|
|
|
142
|
-
const resolvers: Array<{ resolve: (v:any)=>void
|
|
138
|
+
const resolvers: Array<{ resolve: (v: any) => void, reject: (e: any) => void }> = [{ resolve, reject }]
|
|
143
139
|
const timer = setTimeout(async () => {
|
|
144
140
|
const entry = this.pendingWrites.get(id)
|
|
145
|
-
if (!entry)
|
|
141
|
+
if (!entry) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
146
144
|
this.pendingWrites.delete(id)
|
|
147
145
|
try {
|
|
148
146
|
const out = await this._doSetDeviceState(id, entry.body)
|
|
149
147
|
for (const r of entry.resolvers) r.resolve(out)
|
|
150
|
-
} catch (e) {
|
|
148
|
+
} catch (e: any) {
|
|
149
|
+
if (e instanceof SwitchbotAuthenticationError) {
|
|
150
|
+
this.logger?.error?.(`Authentication error for setDeviceState(${id}):`, e.message)
|
|
151
|
+
} else if (e instanceof SwitchbotOperationError) {
|
|
152
|
+
this.logger?.warn?.(`Operation error for setDeviceState(${id}):`, e.message, e.code)
|
|
153
|
+
} else if (e instanceof CharacteristicMissingError) {
|
|
154
|
+
this.logger?.warn?.(`Characteristic missing for setDeviceState(${id}):`, e.characteristic)
|
|
155
|
+
}
|
|
151
156
|
for (const r of entry.resolvers) r.reject(e)
|
|
152
157
|
}
|
|
153
158
|
}, this.writeDebounceMs)
|
|
@@ -157,47 +162,105 @@ export class SwitchBotClient implements ISwitchBotClient {
|
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
private async _doSetDeviceState(id: string, body: any): Promise<any> {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
if (!this.client) {
|
|
166
|
+
throw new SwitchbotOperationError('No SwitchBot client available for setDeviceState', 'no_client')
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const device = await this.getDevice(id)
|
|
170
|
+
if (!device) {
|
|
171
|
+
throw new SwitchbotOperationError(`Device ${id} not found`, 'device_not_found')
|
|
172
|
+
}
|
|
173
|
+
const deviceType = (device.deviceType ?? '').toLowerCase()
|
|
174
|
+
const command = body?.command
|
|
175
|
+
if (!command) {
|
|
176
|
+
throw new SwitchbotOperationError('No command specified in body', 'no_command')
|
|
177
|
+
}
|
|
178
|
+
const handler = getDeviceCommandHandler(deviceType, command)
|
|
179
|
+
if (!handler) {
|
|
180
|
+
throw new SwitchbotOperationError(`Unsupported command '${command}' for device type '${deviceType}'`, 'unsupported_command')
|
|
181
|
+
}
|
|
182
|
+
this.logger?.debug?.(`[${id}] Calling mapped command '${command}' for device type '${deviceType}'`)
|
|
183
|
+
return await handler(device, body)
|
|
184
|
+
} catch (e: any) {
|
|
185
|
+
if (e instanceof SwitchbotAuthenticationError) {
|
|
186
|
+
this.logger?.error?.(`Authentication error for setDeviceState(${id}):`, e.message)
|
|
187
|
+
throw e
|
|
188
|
+
} else if (e instanceof SwitchbotOperationError) {
|
|
189
|
+
this.logger?.warn?.(`Operation error for setDeviceState(${id}):`, e.message, e.code)
|
|
190
|
+
throw e
|
|
191
|
+
} else if (e instanceof CharacteristicMissingError) {
|
|
192
|
+
this.logger?.warn?.(`Characteristic missing for setDeviceState(${id}):`, e.characteristic)
|
|
193
|
+
throw e
|
|
194
|
+
} else {
|
|
195
|
+
this.logger?.warn?.(`Device command failed for ${id}:`, e)
|
|
196
|
+
throw e
|
|
166
197
|
}
|
|
167
198
|
}
|
|
199
|
+
}
|
|
168
200
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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)
|
|
175
207
|
}
|
|
176
208
|
}
|
|
209
|
+
this.pendingWrites.clear()
|
|
177
210
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
211
|
+
if (this.client?.cleanup) {
|
|
212
|
+
await this.client.cleanup()
|
|
213
|
+
}
|
|
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)
|
|
191
229
|
}
|
|
192
230
|
|
|
193
|
-
|
|
231
|
+
return 5000
|
|
194
232
|
}
|
|
195
233
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
234
|
+
private getManagedDevice(id: string): any {
|
|
235
|
+
const manager = (this.client as any)?.devices
|
|
236
|
+
if (manager?.get) {
|
|
237
|
+
return manager.get(id)
|
|
199
238
|
}
|
|
200
|
-
|
|
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 []
|
|
201
249
|
}
|
|
202
|
-
}
|
|
203
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
|
|
265
|
+
}
|
|
266
|
+
}
|