@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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils'
|
|
2
|
+
|
|
3
|
+
import { RequestError } from '@homebridge/plugin-ui-utils'
|
|
4
|
+
import fs from 'node:fs/promises'
|
|
5
|
+
|
|
6
|
+
import { isValidDeviceType } from '../../device-types.js'
|
|
7
|
+
import { getAllDevices, SWITCHBOT_PLATFORM_REGEX } from '../utils/config-parser.js'
|
|
8
|
+
import { validateAndMigrateDeviceType } from '../utils/device-migration.js'
|
|
9
|
+
import { uiLog } from '../utils/logger.js'
|
|
10
|
+
|
|
11
|
+
export function registerConfigEndpoints(server: HomebridgePluginUiServer) {
|
|
12
|
+
/**
|
|
13
|
+
* GET /devices - List all configured devices from Homebridge config
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
server.onRequest('/devices', async () => {
|
|
17
|
+
try {
|
|
18
|
+
const cfgPath = server.homebridgeConfigPath
|
|
19
|
+
if (!cfgPath) {
|
|
20
|
+
throw new Error('HOMEBRIDGE_CONFIG_PATH not set')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const raw = await fs.readFile(cfgPath, 'utf8')
|
|
24
|
+
const cfg = JSON.parse(raw)
|
|
25
|
+
const found: Array<{
|
|
26
|
+
id: string
|
|
27
|
+
name?: string
|
|
28
|
+
type?: string
|
|
29
|
+
connectionPreference?: string
|
|
30
|
+
room?: string
|
|
31
|
+
typeValidationWarning?: string
|
|
32
|
+
}> = []
|
|
33
|
+
const invalidTypeDevices: Array<{ name: string, type: string, suggestion?: string }> = []
|
|
34
|
+
|
|
35
|
+
const platforms = Array.isArray(cfg.platforms) ? cfg.platforms : []
|
|
36
|
+
for (const p of platforms) {
|
|
37
|
+
try {
|
|
38
|
+
const platformName = p.platform || p.name || ''
|
|
39
|
+
// Match known SwitchBot platform identifiers
|
|
40
|
+
if (!platformName || !SWITCHBOT_PLATFORM_REGEX.test(String(platformName))) {
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const devices = getAllDevices(p)
|
|
45
|
+
for (const d of devices) {
|
|
46
|
+
const id = d.deviceId ?? d.id
|
|
47
|
+
if (!id) {
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const deviceType = d.configDeviceType ?? d.type ?? d.deviceType
|
|
52
|
+
const deviceName = d.configDeviceName ?? d.name ?? d.deviceName
|
|
53
|
+
|
|
54
|
+
// Validate device type
|
|
55
|
+
const deviceObj = { configDeviceType: deviceType, configDeviceName: deviceName, deviceId: id }
|
|
56
|
+
const validationResult = validateAndMigrateDeviceType(deviceObj, false)
|
|
57
|
+
|
|
58
|
+
const deviceEntry: typeof found[0] = {
|
|
59
|
+
id,
|
|
60
|
+
name: deviceName,
|
|
61
|
+
type: deviceType,
|
|
62
|
+
connectionPreference: d.connectionPreference ?? d.connection ?? 'auto',
|
|
63
|
+
room: d.room || d.location || undefined,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!isValidDeviceType(deviceType)) {
|
|
67
|
+
deviceEntry.typeValidationWarning = `Invalid type "${deviceType}"`
|
|
68
|
+
if (validationResult.correctedType) {
|
|
69
|
+
deviceEntry.typeValidationWarning += ` - should be "${validationResult.correctedType}"`
|
|
70
|
+
invalidTypeDevices.push({
|
|
71
|
+
name: deviceName,
|
|
72
|
+
type: deviceType,
|
|
73
|
+
suggestion: validationResult.correctedType,
|
|
74
|
+
})
|
|
75
|
+
} else {
|
|
76
|
+
invalidTypeDevices.push({
|
|
77
|
+
name: deviceName,
|
|
78
|
+
type: deviceType,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
found.push(deviceEntry)
|
|
84
|
+
}
|
|
85
|
+
} catch (e) {
|
|
86
|
+
// ignore malformed platform entries
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Log validation issues
|
|
91
|
+
if (invalidTypeDevices.length > 0) {
|
|
92
|
+
uiLog.warn(
|
|
93
|
+
`Found ${invalidTypeDevices.length} device(s) with invalid types:\n${invalidTypeDevices.map(d => ` - "${d.name}": "${d.type}"${d.suggestion ? ` → should be "${d.suggestion}"` : ''}`).join('\n')}`,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
uiLog.info(`GET /devices - Found ${found.length} devices in ${cfgPath}${invalidTypeDevices.length > 0 ? ` (${invalidTypeDevices.length} with validation warnings)` : ''}`)
|
|
98
|
+
return {
|
|
99
|
+
success: true,
|
|
100
|
+
data: found,
|
|
101
|
+
...(invalidTypeDevices.length > 0 && { validationWarnings: invalidTypeDevices }),
|
|
102
|
+
}
|
|
103
|
+
} catch (e) {
|
|
104
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
105
|
+
uiLog.error(`Error in /devices: ${msg}`)
|
|
106
|
+
// Pass the real error message to the frontend for better diagnostics
|
|
107
|
+
throw new RequestError(msg || 'Failed to read Homebridge config', e)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils'
|
|
2
|
+
|
|
3
|
+
import { SwitchBotClient } from '../../switchbotClient.js'
|
|
4
|
+
import { getAllDevices, getDevicesRef, getSwitchBotPlatformConfig } from '../utils/config-parser.js'
|
|
5
|
+
import { uiLog } from '../utils/logger.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register device CRUD endpoints
|
|
9
|
+
*/
|
|
10
|
+
export function registerDeviceEndpoints(server: HomebridgePluginUiServer) {
|
|
11
|
+
/**
|
|
12
|
+
* POST /add-devices - Add or update multiple devices in the Homebridge config
|
|
13
|
+
* Expects: { devices: Array<{ deviceId, configDeviceType, configDeviceName, ... }> }
|
|
14
|
+
*/
|
|
15
|
+
server.onRequest('/add-devices', async (body: any) => {
|
|
16
|
+
try {
|
|
17
|
+
if (!body || !Array.isArray(body.devices) || body.devices.length === 0) {
|
|
18
|
+
throw new Error('No devices provided')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { platform, cfg, cfgPath } = await getSwitchBotPlatformConfig(server)
|
|
22
|
+
const devicesRef = getDevicesRef(platform)
|
|
23
|
+
const incomingDevices = body.devices
|
|
24
|
+
let added = 0
|
|
25
|
+
let updated = 0
|
|
26
|
+
|
|
27
|
+
for (const newDev of incomingDevices) {
|
|
28
|
+
const id = String(newDev.deviceId || newDev.id || '').trim().toLowerCase()
|
|
29
|
+
if (!id) {
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
const idx = devicesRef.findIndex((d: any) => String(d.deviceId || d.id || '').trim().toLowerCase() === id)
|
|
33
|
+
if (idx >= 0) {
|
|
34
|
+
// Update existing device
|
|
35
|
+
devicesRef[idx] = { ...devicesRef[idx], ...newDev }
|
|
36
|
+
updated++
|
|
37
|
+
} else {
|
|
38
|
+
// Add new device
|
|
39
|
+
devicesRef.push({ ...newDev })
|
|
40
|
+
added++
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Log devicesRef and cfgPath before saving
|
|
45
|
+
uiLog.info(`[DEBUG] devicesRef before save:`, JSON.stringify(devicesRef, null, 2))
|
|
46
|
+
uiLog.info(`[DEBUG] cfgPath:`, cfgPath)
|
|
47
|
+
await import('../utils/config-parser.js').then(m => m.saveConfig(cfgPath, cfg))
|
|
48
|
+
|
|
49
|
+
// Log the config file contents after saving for debugging
|
|
50
|
+
try {
|
|
51
|
+
const fs = await import('node:fs/promises')
|
|
52
|
+
const raw = await fs.readFile(cfgPath, 'utf-8')
|
|
53
|
+
uiLog.info(`[DEBUG] Config after add-devices save:`, raw)
|
|
54
|
+
} catch (e) {
|
|
55
|
+
uiLog.warn(`[DEBUG] Could not read config after save:`, e)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
uiLog.info(`POST /add-devices - Added: ${added}, Updated: ${updated}`)
|
|
59
|
+
return {
|
|
60
|
+
success: true,
|
|
61
|
+
data: {
|
|
62
|
+
added,
|
|
63
|
+
updated,
|
|
64
|
+
total: devicesRef.length,
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
69
|
+
uiLog.error(`POST /add-devices failed: ${msg}`)
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
error: msg,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
/**
|
|
77
|
+
* POST /test-connection - Test connectivity and basic read for a device
|
|
78
|
+
*/
|
|
79
|
+
server.onRequest('/test-connection', async (body: any) => {
|
|
80
|
+
let client: SwitchBotClient | null = null
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const deviceId = String(body?.deviceId || '').trim()
|
|
84
|
+
if (!deviceId) {
|
|
85
|
+
throw new Error('Device ID is required')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const { platform } = await getSwitchBotPlatformConfig(server)
|
|
89
|
+
const allDevices = getAllDevices(platform)
|
|
90
|
+
const normalizedDeviceId = deviceId.toLowerCase()
|
|
91
|
+
const configuredDevice = allDevices.find((d: any) => String(d.deviceId ?? d.id ?? '').trim().toLowerCase() === normalizedDeviceId)
|
|
92
|
+
|
|
93
|
+
const startedAt = Date.now()
|
|
94
|
+
|
|
95
|
+
client = new SwitchBotClient({
|
|
96
|
+
...(platform as any),
|
|
97
|
+
logger: uiLog,
|
|
98
|
+
} as any)
|
|
99
|
+
|
|
100
|
+
await client.init()
|
|
101
|
+
const raw = await client.getDevice(deviceId)
|
|
102
|
+
const latencyMs = Date.now() - startedAt
|
|
103
|
+
|
|
104
|
+
const state = raw?.body ?? raw
|
|
105
|
+
const stateConnection = String(state?.connectionType || state?.source || body?.connectionType || '').toLowerCase()
|
|
106
|
+
const method = stateConnection.includes('ble')
|
|
107
|
+
? 'BLE'
|
|
108
|
+
: stateConnection.includes('api')
|
|
109
|
+
? 'OpenAPI'
|
|
110
|
+
: 'Auto'
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
success: true,
|
|
114
|
+
data: {
|
|
115
|
+
success: true,
|
|
116
|
+
deviceId,
|
|
117
|
+
method,
|
|
118
|
+
latencyMs,
|
|
119
|
+
message: configuredDevice
|
|
120
|
+
? `Connected to "${configuredDevice.configDeviceName || configuredDevice.deviceId || deviceId}"`
|
|
121
|
+
: 'Connected successfully',
|
|
122
|
+
state: {
|
|
123
|
+
online: state?.online,
|
|
124
|
+
power: state?.power,
|
|
125
|
+
battery: state?.battery,
|
|
126
|
+
version: state?.version,
|
|
127
|
+
deviceType: state?.deviceType,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
} catch (e) {
|
|
132
|
+
const message = e instanceof Error ? e.message : String(e)
|
|
133
|
+
uiLog.warn(`POST /test-connection failed: ${message}`)
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
success: true,
|
|
137
|
+
data: {
|
|
138
|
+
success: false,
|
|
139
|
+
deviceId: String(body?.deviceId || ''),
|
|
140
|
+
method: 'Auto',
|
|
141
|
+
latencyMs: 0,
|
|
142
|
+
message,
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
} finally {
|
|
146
|
+
try {
|
|
147
|
+
await client?.destroy()
|
|
148
|
+
} catch (_e) {
|
|
149
|
+
// Ignore client shutdown errors
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import type { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils'
|
|
2
|
+
|
|
3
|
+
import { RequestError } from '@homebridge/plugin-ui-utils'
|
|
4
|
+
|
|
5
|
+
import { getCredential, getSwitchBotPlatformConfig } from '../utils/config-parser.js'
|
|
6
|
+
import { uiLog } from '../utils/logger.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Register discovery endpoint
|
|
10
|
+
* Discovers SwitchBot devices via BLE and OpenAPI
|
|
11
|
+
*/
|
|
12
|
+
export function registerDiscoveryEndpoint(server: HomebridgePluginUiServer) {
|
|
13
|
+
server.onRequest('/ble-status', async () => {
|
|
14
|
+
try {
|
|
15
|
+
const { platform } = await getSwitchBotPlatformConfig(server)
|
|
16
|
+
const token = getCredential(platform, 'openApiToken') || platform.token
|
|
17
|
+
const secret = getCredential(platform, 'openApiSecret') || platform.secret
|
|
18
|
+
const { SwitchBot } = await import('node-switchbot')
|
|
19
|
+
const switchbot = new SwitchBot({
|
|
20
|
+
token: token || undefined,
|
|
21
|
+
secret: secret || undefined,
|
|
22
|
+
enableBLE: true,
|
|
23
|
+
enableFallback: true,
|
|
24
|
+
enableRetry: true,
|
|
25
|
+
enableCircuitBreaker: true,
|
|
26
|
+
enableConnectionIntelligence: true,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
await switchbot.discover({ timeout: 1000 })
|
|
30
|
+
return { success: true, data: { available: true, message: 'adapter ready' } }
|
|
31
|
+
} catch (e) {
|
|
32
|
+
const message = e instanceof Error ? e.message : String(e)
|
|
33
|
+
uiLog.warn(`GET /ble-status failed: ${message}`)
|
|
34
|
+
return { success: true, data: { available: false, message } }
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
server.onRequest('/discover', async (payload?: any) => {
|
|
39
|
+
uiLog.debug(`[SwitchBot UI/Server] /discover incoming payload: ${JSON.stringify(payload)}`)
|
|
40
|
+
try {
|
|
41
|
+
const { platform } = await getSwitchBotPlatformConfig(server)
|
|
42
|
+
const mode = String(payload?.mode || 'all').toLowerCase()
|
|
43
|
+
// Only run BLE if mode is all/ble AND bleEnabled is not false (default true if missing)
|
|
44
|
+
const bleEnabled = payload?.bleEnabled !== false;
|
|
45
|
+
const runBle = (mode === 'all' || mode === 'ble') && bleEnabled;
|
|
46
|
+
const runOpenApi = mode === 'all' || mode === 'openapi'
|
|
47
|
+
const bleScanDurationSeconds = Math.max(3, Math.min(15, Number(payload?.bleScanDurationSeconds || 5)))
|
|
48
|
+
const bleTimeoutSeconds = Math.max(3, Math.min(30, Number(payload?.bleTimeoutSeconds || 8)))
|
|
49
|
+
|
|
50
|
+
uiLog.debug(`GET /discover - Platform config keys: ${Object.keys(platform).join(', ')}`)
|
|
51
|
+
|
|
52
|
+
const token = getCredential(platform, 'openApiToken') || platform.token
|
|
53
|
+
const secret = getCredential(platform, 'openApiSecret') || platform.secret
|
|
54
|
+
|
|
55
|
+
const hasOpenAPICredentials = !!(token && secret)
|
|
56
|
+
|
|
57
|
+
if (!hasOpenAPICredentials) {
|
|
58
|
+
uiLog.warn('GET /discover - No OpenAPI credentials found, will attempt BLE-only discovery')
|
|
59
|
+
} else {
|
|
60
|
+
uiLog.info('GET /discover - Using OpenAPI credentials for discovery')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Import and initialize node-switchbot
|
|
64
|
+
const { SwitchBot } = await import('node-switchbot')
|
|
65
|
+
const switchbot = new SwitchBot({
|
|
66
|
+
token: token || undefined,
|
|
67
|
+
secret: secret || undefined,
|
|
68
|
+
enableBLE: true,
|
|
69
|
+
enableFallback: true,
|
|
70
|
+
enableRetry: true,
|
|
71
|
+
enableCircuitBreaker: true,
|
|
72
|
+
enableConnectionIntelligence: true,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const deviceMap = new Map<string, any>()
|
|
76
|
+
|
|
77
|
+
// 1. Try BLE discovery first (with timeout)
|
|
78
|
+
if (runBle) {
|
|
79
|
+
uiLog.info('GET /discover - Starting BLE scan...')
|
|
80
|
+
try {
|
|
81
|
+
const bleTimeout = bleTimeoutSeconds * 1000
|
|
82
|
+
const bleDiscoveryPromise = switchbot.discover({ timeout: bleScanDurationSeconds * 1000 })
|
|
83
|
+
const bleDevices = await Promise.race([
|
|
84
|
+
bleDiscoveryPromise,
|
|
85
|
+
new Promise<any[]>(resolve => setTimeout(resolve, bleTimeout, [])),
|
|
86
|
+
])
|
|
87
|
+
|
|
88
|
+
if (Array.isArray(bleDevices) && bleDevices.length > 0) {
|
|
89
|
+
uiLog.info(`GET /discover - Found ${bleDevices.length} BLE devices`)
|
|
90
|
+
for (const [index, d] of bleDevices.entries()) {
|
|
91
|
+
const info = typeof d?.getInfo === 'function' ? d.getInfo() : undefined
|
|
92
|
+
const id = d?.id
|
|
93
|
+
|| (typeof d?.getId === 'function' ? d.getId() : undefined)
|
|
94
|
+
|| info?.id
|
|
95
|
+
|
|
96
|
+
const mac = d?.mac
|
|
97
|
+
|| (typeof d?.getMAC === 'function' ? d.getMAC() : undefined)
|
|
98
|
+
|| info?.mac
|
|
99
|
+
|
|
100
|
+
if (!id) {
|
|
101
|
+
uiLog.warn(`GET /discover - BLE device at index ${index} has no id, skipping`)
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const connectionTypes = Array.isArray(info?.connectionTypes) ? info.connectionTypes : []
|
|
106
|
+
const isHybrid = connectionTypes.includes('api')
|
|
107
|
+
|
|
108
|
+
deviceMap.set(id, {
|
|
109
|
+
id,
|
|
110
|
+
name: d?.name || (typeof d?.getName === 'function' ? d.getName() : undefined) || info?.name || d?.deviceName || id,
|
|
111
|
+
type: d?.deviceType || (typeof d?.getDeviceType === 'function' ? d.getDeviceType() : undefined) || info?.deviceType || d?.type || d?.model || 'unknown',
|
|
112
|
+
model: info?.model || d?.model || d?.deviceModel,
|
|
113
|
+
address: mac,
|
|
114
|
+
connectionType: isHybrid ? 'Both' : 'BLE',
|
|
115
|
+
rssi: info?.rssi || d?.rssi,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
uiLog.info('GET /discover - No BLE devices found or scan timed out')
|
|
120
|
+
}
|
|
121
|
+
} catch (bleErr) {
|
|
122
|
+
uiLog.warn(`GET /discover - BLE discovery failed: ${bleErr instanceof Error ? bleErr.message : String(bleErr)}`)
|
|
123
|
+
// Continue with OpenAPI even if BLE fails
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 2. Get devices from OpenAPI (only if credentials are available)
|
|
128
|
+
if (runOpenApi && hasOpenAPICredentials) {
|
|
129
|
+
uiLog.info('GET /discover - Fetching devices from OpenAPI...')
|
|
130
|
+
try {
|
|
131
|
+
const apiClient = switchbot.getAPIClient()
|
|
132
|
+
if (!apiClient) {
|
|
133
|
+
throw new Error('API client not available - token/secret may be missing')
|
|
134
|
+
}
|
|
135
|
+
const apiData = await apiClient.getDevices()
|
|
136
|
+
uiLog.debug(`GET /discover - OpenAPI response: ${JSON.stringify(apiData)}`)
|
|
137
|
+
|
|
138
|
+
// Parse physical devices - apiData is DeviceListResponse with deviceList and infraredRemoteList
|
|
139
|
+
const devices = apiData.deviceList || []
|
|
140
|
+
const irDevices = apiData.infraredRemoteList || []
|
|
141
|
+
|
|
142
|
+
uiLog.info(`GET /discover - Found ${devices.length} OpenAPI physical devices and ${irDevices.length} IR devices`)
|
|
143
|
+
|
|
144
|
+
// Process physical devices from OpenAPI
|
|
145
|
+
for (const d of devices) {
|
|
146
|
+
const id = d.deviceId
|
|
147
|
+
if (!id) {
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const existing = deviceMap.get(id)
|
|
152
|
+
if (existing) {
|
|
153
|
+
// Device found via both BLE and OpenAPI
|
|
154
|
+
existing.connectionType = 'Both'
|
|
155
|
+
existing.name = d.deviceName || existing.name
|
|
156
|
+
existing.type = d.deviceType || existing.type
|
|
157
|
+
// Note: APIDevice doesn't have a model property
|
|
158
|
+
existing.enabled = d.enableCloudService !== false
|
|
159
|
+
existing.hubDeviceId = d.hubDeviceId
|
|
160
|
+
} else {
|
|
161
|
+
// Device only found via OpenAPI
|
|
162
|
+
deviceMap.set(id, {
|
|
163
|
+
id,
|
|
164
|
+
name: d.deviceName || id,
|
|
165
|
+
type: d.deviceType || 'unknown',
|
|
166
|
+
enabled: d.enableCloudService !== false,
|
|
167
|
+
hubDeviceId: d.hubDeviceId,
|
|
168
|
+
connectionType: 'OpenAPI',
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Process IR devices (OpenAPI only)
|
|
174
|
+
for (const d of irDevices) {
|
|
175
|
+
const id = d.deviceId
|
|
176
|
+
if (!id) {
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
deviceMap.set(id, {
|
|
181
|
+
id,
|
|
182
|
+
name: d.deviceName || id,
|
|
183
|
+
type: d.remoteType || 'unknown',
|
|
184
|
+
enabled: true,
|
|
185
|
+
hubDeviceId: d.hubDeviceId,
|
|
186
|
+
connectionType: 'OpenAPI',
|
|
187
|
+
isIR: true,
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
} catch (apiErr) {
|
|
191
|
+
uiLog.error(`GET /discover - OpenAPI discovery failed: ${apiErr instanceof Error ? apiErr.message : String(apiErr)}`)
|
|
192
|
+
// If we have BLE devices, we can still return those
|
|
193
|
+
if (deviceMap.size === 0) {
|
|
194
|
+
throw apiErr
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} else if (runOpenApi) {
|
|
198
|
+
uiLog.info('GET /discover - Skipping OpenAPI discovery (no credentials configured)')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const normalizedBleDevices = [...deviceMap.values()].filter(d => d.connectionType === 'BLE' || d.connectionType === 'Both')
|
|
202
|
+
const firstNormalizedBleId = normalizedBleDevices[0]?.id || 'none'
|
|
203
|
+
uiLog.info(`GET /discover - Normalized BLE devices: ${normalizedBleDevices.length} (firstId: ${firstNormalizedBleId})`)
|
|
204
|
+
|
|
205
|
+
// Check if we found any devices
|
|
206
|
+
if (deviceMap.size === 0 && mode === 'openapi' && !hasOpenAPICredentials) {
|
|
207
|
+
return { success: true, data: [] }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (deviceMap.size === 0) {
|
|
211
|
+
const errorMsg = hasOpenAPICredentials
|
|
212
|
+
? 'No devices found via BLE or OpenAPI. Make sure devices are powered on and in range.'
|
|
213
|
+
: 'No devices found via BLE. OpenAPI credentials not configured. Please save your credentials in settings to discover cloud-connected devices, or ensure BLE devices are powered on and in range.'
|
|
214
|
+
uiLog.error(`GET /discover - ${errorMsg}`)
|
|
215
|
+
throw new Error(errorMsg)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Convert map to array
|
|
219
|
+
const allDiscovered = [...deviceMap.values()]
|
|
220
|
+
|
|
221
|
+
const bleCount = allDiscovered.filter(d => d.connectionType === 'BLE').length
|
|
222
|
+
const apiCount = allDiscovered.filter(d => d.connectionType === 'OpenAPI').length
|
|
223
|
+
const bothCount = allDiscovered.filter(d => d.connectionType === 'Both').length
|
|
224
|
+
|
|
225
|
+
uiLog.info(`GET /discover - Total: ${allDiscovered.length} devices (BLE: ${bleCount}, OpenAPI: ${apiCount}, Both: ${bothCount})`)
|
|
226
|
+
return { success: true, data: allDiscovered }
|
|
227
|
+
} catch (e) {
|
|
228
|
+
if (e instanceof Error) {
|
|
229
|
+
uiLog.error(`Error in /discover: ${e.message}`)
|
|
230
|
+
if (e.stack) {
|
|
231
|
+
uiLog.error(`[Stack] ${e.stack}`)
|
|
232
|
+
}
|
|
233
|
+
uiLog.error(`[Object]`, e)
|
|
234
|
+
} else {
|
|
235
|
+
uiLog.error(`Error in /discover: ${String(e)}`)
|
|
236
|
+
}
|
|
237
|
+
throw new RequestError(`Failed to discover devices: ${e instanceof Error ? e.message : String(e)}`, e)
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
}
|