@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
|
@@ -1,639 +1,215 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
2
|
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
|
-
<title>SwitchBot Plugin UI</title>
|
|
7
|
-
<style>
|
|
8
|
-
:root {
|
|
9
|
-
color-scheme: light dark;
|
|
10
|
-
}
|
|
11
3
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
background: #fff;
|
|
19
|
-
border-color: #ddd;
|
|
20
|
-
color: #1a1a1a;
|
|
21
|
-
}
|
|
22
|
-
input:focus {
|
|
23
|
-
border-color: #6366f1;
|
|
24
|
-
}
|
|
25
|
-
.card {
|
|
26
|
-
background: #fff;
|
|
27
|
-
border: 1px solid #ddd;
|
|
28
|
-
}
|
|
29
|
-
code {
|
|
30
|
-
background: #f0f0f0;
|
|
31
|
-
color: #1a1a1a;
|
|
32
|
-
}
|
|
33
|
-
h2 {
|
|
34
|
-
border-bottom-color: #ddd;
|
|
35
|
-
}
|
|
36
|
-
.status {
|
|
37
|
-
color: #666;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
7
|
+
<title>SwitchBot Plugin UI</title>
|
|
8
|
+
<link rel="stylesheet" href="css/styles.css" />
|
|
9
|
+
</head>
|
|
40
10
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
background: #1a1a1a;
|
|
44
|
-
color: #fff;
|
|
45
|
-
}
|
|
46
|
-
input {
|
|
47
|
-
background: #2a2a2a;
|
|
48
|
-
border-color: #444;
|
|
49
|
-
color: #fff;
|
|
50
|
-
}
|
|
51
|
-
.card {
|
|
52
|
-
background: #2a2a2a;
|
|
53
|
-
}
|
|
54
|
-
code {
|
|
55
|
-
background: #333;
|
|
56
|
-
color: #fff;
|
|
57
|
-
}
|
|
58
|
-
h2 {
|
|
59
|
-
border-bottom-color: #444;
|
|
60
|
-
}
|
|
61
|
-
.status {
|
|
62
|
-
color: #888;
|
|
63
|
-
}
|
|
64
|
-
.device-item {
|
|
65
|
-
background: #333;
|
|
66
|
-
border: 1px solid #444;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
11
|
+
<body>
|
|
12
|
+
<h1>🤖 SwitchBot Configuration</h1>
|
|
69
13
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
border: 1px solid #e0e0e0;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
body { font-family: system-ui, -apple-system, Arial; padding: 16px }
|
|
78
|
-
h1 { font-size: 24px; margin-top:0 }
|
|
79
|
-
h2 { font-size: 16px; margin-top: 24px; margin-bottom: 12px; border-bottom: 1px solid; padding-bottom:8px }
|
|
80
|
-
ul { padding-left: 0; list-style: none }
|
|
81
|
-
li { margin: 8px 0; display:flex; gap:8px; align-items:center }
|
|
82
|
-
button { padding: 8px 16px; background:#6366f1; color:#fff; border:none; border-radius:4px; cursor:pointer }
|
|
83
|
-
button:hover { background:#4f46e5 }
|
|
84
|
-
button.success { background:#10b981 }
|
|
85
|
-
code { padding:4px 6px; border-radius:4px; font-family: monospace }
|
|
86
|
-
.form-group { margin-bottom:16px }
|
|
87
|
-
label { display:block; margin-bottom:6px; font-weight:500 }
|
|
88
|
-
input { width:100%; max-width:400px; padding:8px; border: 1px solid; border-radius:4px; font-family:monospace }
|
|
89
|
-
input:focus { outline:none }
|
|
90
|
-
.status { font-size:14px; margin-top:4px }
|
|
91
|
-
.status.ok { color:#10b981 }
|
|
92
|
-
.error { color:#ef4444 }
|
|
93
|
-
.success-msg { color:#10b981; margin-top:8px }
|
|
94
|
-
.card { padding:16px; border-radius:8px; margin-bottom:16px }
|
|
95
|
-
.device-item { padding:8px; border-radius:4px; margin-bottom:8px; display:flex; gap:8px; align-items:center; justify-content:space-between }
|
|
96
|
-
</style>
|
|
97
|
-
</head>
|
|
98
|
-
<body>
|
|
99
|
-
<h1>🤖 SwitchBot Configuration</h1>
|
|
100
|
-
|
|
101
|
-
<div class="card">
|
|
102
|
-
<h2>API Credentials</h2>
|
|
103
|
-
<p>Configure your SwitchBot API token and secret to enable device discovery and control.</p>
|
|
104
|
-
|
|
14
|
+
<div class="card compact">
|
|
15
|
+
<h2>API Credentials</h2>
|
|
16
|
+
<div class="credentials-row">
|
|
105
17
|
<div class="form-group">
|
|
106
|
-
<label for="token">
|
|
107
|
-
<input type="password" id="token" placeholder="Enter
|
|
18
|
+
<label for="token">Token:</label>
|
|
19
|
+
<input type="password" id="token" placeholder="Enter token" />
|
|
108
20
|
<div class="status" id="tokenStatus"></div>
|
|
109
21
|
</div>
|
|
110
|
-
|
|
111
22
|
<div class="form-group">
|
|
112
|
-
<label for="secret">
|
|
113
|
-
<input type="password" id="secret" placeholder="Enter
|
|
23
|
+
<label for="secret">Secret:</label>
|
|
24
|
+
<input type="password" id="secret" placeholder="Enter secret" />
|
|
114
25
|
<div class="status" id="secretStatus"></div>
|
|
115
26
|
</div>
|
|
116
|
-
|
|
117
|
-
<button id="saveBtn" onclick="saveCredentials()">Save Credentials</button>
|
|
118
|
-
<div id="saveStatus"></div>
|
|
119
27
|
</div>
|
|
120
|
-
|
|
121
|
-
<div
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
28
|
+
<button id="saveBtn" onclick="saveCredentials()">Save</button>
|
|
29
|
+
<div id="saveStatus"></div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="card compact">
|
|
33
|
+
<h2>Discover Devices</h2>
|
|
34
|
+
<div style="margin-bottom: 6px">
|
|
35
|
+
<label style="display: flex; align-items: center; gap: 4px; cursor: pointer; font-size: 11px">
|
|
36
|
+
<input type="checkbox" id="autoAddAllCheckbox" style="width: auto; margin: 0" />
|
|
37
|
+
<span>Auto-add all</span>
|
|
38
|
+
<span
|
|
39
|
+
title="When enabled, Discover immediately adds all found devices to your config. Existing devices are skipped automatically."
|
|
40
|
+
style="font-size: 11px; opacity: 0.7">ⓘ</span>
|
|
41
|
+
</label>
|
|
42
|
+
<div style="margin-top: 2px; margin-left: 22px; font-size: 10px; opacity: 0.75; line-height: 1.25">
|
|
43
|
+
Adds all discovered devices automatically; already configured devices are skipped.
|
|
129
44
|
</div>
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
<
|
|
133
|
-
<
|
|
134
|
-
<
|
|
45
|
+
</div>
|
|
46
|
+
<div class="discovery-settings-row" style="margin-bottom: 2px">
|
|
47
|
+
<label id="bleScanSetting" style="display: inline-flex; align-items: center; gap: 4px; font-size: 11px">
|
|
48
|
+
<span>Scan:</span>
|
|
49
|
+
<select id="bleScanDurationSelect" style="font-size: 11px; padding: 2px 6px">
|
|
50
|
+
<option value="3">3s</option>
|
|
51
|
+
<option value="5" selected>5s</option>
|
|
52
|
+
<option value="10">10s</option>
|
|
53
|
+
<option value="15">15s</option>
|
|
54
|
+
</select>
|
|
55
|
+
</label>
|
|
56
|
+
<label id="bleTimeoutSetting" style="display: inline-flex; align-items: center; gap: 4px; font-size: 11px">
|
|
57
|
+
<span>Timeout:</span>
|
|
58
|
+
<input id="bleTimeoutInput" type="number" min="3" max="30" step="1" value="8"
|
|
59
|
+
style="width: 56px; font-size: 11px; padding: 2px 4px" />
|
|
60
|
+
<span>s</span>
|
|
61
|
+
</label>
|
|
62
|
+
<label style="display: flex; align-items: center; gap: 4px; cursor: pointer; font-size: 11px">
|
|
63
|
+
<input type="checkbox" id="disableBleScanCheckbox" style="width: auto; margin: 0" />
|
|
64
|
+
<span>Disable BLE scan</span>
|
|
65
|
+
</label>
|
|
66
|
+
</div>
|
|
67
|
+
<div id="bluetoothStatus" style="margin-bottom: 4px; font-size: 10px; opacity: 0.8">Bluetooth: checking...</div>
|
|
68
|
+
<div class="discovery-settings-row" style="margin-bottom: 8px">
|
|
69
|
+
<div id="lastScannedStatus" style="font-size: 10px; opacity: 0.8; flex: 1 1 auto">Last scanned: never</div>
|
|
70
|
+
<label style="font-size: 11px">
|
|
71
|
+
Auto-refresh:
|
|
72
|
+
<select id="autoRefreshIntervalSelect" style="margin-left: 4px; font-size: 11px; padding: 2px 6px">
|
|
73
|
+
<option value="0" selected>Off</option>
|
|
74
|
+
<option value="30">30s</option>
|
|
75
|
+
<option value="60">1m</option>
|
|
76
|
+
<option value="300">5m</option>
|
|
77
|
+
</select>
|
|
78
|
+
</label>
|
|
79
|
+
<button id="refreshDiscoverBtn" class="secondary" style="padding: 4px 9px; font-size: 11px">Refresh</button>
|
|
80
|
+
</div>
|
|
81
|
+
<button id="discoverBtn" onclick="discoverDevices()" style="margin-bottom: 8px">
|
|
82
|
+
🔍 Discover
|
|
83
|
+
</button>
|
|
84
|
+
<button id="cancelDiscoverBtn"
|
|
85
|
+
style="margin-left: 6px; margin-bottom: 8px; display: none; padding: 6px 10px; font-size: 11px" class="secondary">
|
|
86
|
+
Cancel
|
|
87
|
+
</button>
|
|
88
|
+
<div id="discoverStatus" style="margin-bottom: 6px; font-size: 11px"></div>
|
|
89
|
+
<div id="discoverDevicesFound" style="margin-bottom: 6px; font-size: 11px; display: none"></div>
|
|
90
|
+
<div id="discoverPhaseProgress" class="discover-phase-progress" style="display: none">
|
|
91
|
+
<div class="discover-phase-track">
|
|
92
|
+
<div id="discoverPhaseFill" class="discover-phase-fill"></div>
|
|
135
93
|
</div>
|
|
94
|
+
<div id="discoverPhaseLabel" class="discover-phase-label"></div>
|
|
136
95
|
</div>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
<
|
|
140
|
-
<p>This page lists devices found in your Homebridge config for the SwitchBot platform. Use the copy button to insert device IDs into the plugin configuration. Connection preference (BLE/OpenAPI) is shown when available.</p>
|
|
141
|
-
<div id="status">Loading…</div>
|
|
142
|
-
<ul id="devices"></ul>
|
|
96
|
+
<div id="discoveredList">
|
|
97
|
+
<h3 style="margin-top: 0; font-size: 12px; margin-bottom: 6px; font-weight: 600">Available Devices</h3>
|
|
98
|
+
<ul id="discoveredDevices" style="max-height: 300px; overflow-y: auto"></ul>
|
|
143
99
|
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div class="card compact">
|
|
103
|
+
<h2>Configured Devices</h2>
|
|
104
|
+
<div id="status">Loading…</div>
|
|
105
|
+
<ul id="devices"></ul>
|
|
106
|
+
<!-- BLE encryption key fields will be rendered dynamically per device in the UI (see app.ts/devices.js) -->
|
|
107
|
+
<div id="removeAllContainer" style="
|
|
108
|
+
display: none;
|
|
109
|
+
margin-top: 12px;
|
|
110
|
+
padding-top: 10px;
|
|
111
|
+
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
|
112
|
+
">
|
|
113
|
+
<button id="removeAllBtn" style="
|
|
114
|
+
background: #dc2626;
|
|
115
|
+
width: 100%;
|
|
116
|
+
padding: 7px 12px;
|
|
117
|
+
font-size: 12px;
|
|
118
|
+
font-weight: 500;
|
|
119
|
+
">
|
|
120
|
+
🗑️ Remove All Devices
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
<div class="card compact" id="advancedSettingsCard" style="margin-top: 24px;">
|
|
127
|
+
<h2>Advanced Settings</h2>
|
|
128
|
+
<form id="advancedSettingsForm">
|
|
129
|
+
<div class="form-group">
|
|
130
|
+
<label for="enableMatter">
|
|
131
|
+
<input type="checkbox" id="enableMatter" /> Enable Matter Support (Override)
|
|
132
|
+
</label>
|
|
133
|
+
<small>Manually enable Matter support. By default, Matter is auto-detected from the child bridge configuration. Only change this if you need to override the auto-detection.</small>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="form-group">
|
|
136
|
+
<label for="preferMatter">
|
|
137
|
+
<input type="checkbox" id="preferMatter" /> Prefer Matter when available
|
|
138
|
+
</label>
|
|
139
|
+
<small>If enabled and Matter is available, devices will be presented as Matter accessories where supported (instead of HAP).</small>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="form-group">
|
|
142
|
+
<label for="enableBLE">
|
|
143
|
+
<input type="checkbox" id="enableBLE" /> Enable BLE (Bluetooth)
|
|
144
|
+
</label>
|
|
145
|
+
<small>Enable or disable BLE (Bluetooth Low Energy) support. If disabled, only OpenAPI (cloud) will be used.</small>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="form-group">
|
|
148
|
+
<label for="blePollingEnabled">
|
|
149
|
+
<input type="checkbox" id="blePollingEnabled" /> Enable BLE Polling Fallback
|
|
150
|
+
</label>
|
|
151
|
+
<small>If enabled, the plugin will periodically poll BLE devices for status as a safety net in addition to real-time notifications. This helps recover from missed notifications or connection loss. Recommended for reliability. Can be overridden per device.</small>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="form-group">
|
|
154
|
+
<label for="blePollIntervalMs">BLE Polling Interval (ms)</label>
|
|
155
|
+
<input type="number" id="blePollIntervalMs" min="60000" max="3600000" step="1000" value="600000" />
|
|
156
|
+
<small>How often to poll BLE devices for status (in milliseconds). Default is 600000 (10 minutes). Set higher to reduce battery drain. Minimum 60000 (1 minute). Can be overridden per device.</small>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="form-group">
|
|
159
|
+
<label for="openApiRefreshRate">OpenAPI Polling Interval (seconds)</label>
|
|
160
|
+
<input type="number" id="openApiRefreshRate" min="30" max="86400" step="1" value="300" />
|
|
161
|
+
<small>How often to poll devices via OpenAPI for status. Default: 300 (5 min). Min: 30. Can be overridden per device.</small>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="form-group">
|
|
164
|
+
<label for="matterBatchEnabled">
|
|
165
|
+
<input type="checkbox" id="matterBatchEnabled" /> Enable Batched OpenAPI Polling
|
|
166
|
+
</label>
|
|
167
|
+
<small>Poll all OpenAPI devices in a single batch at the configured interval. Devices with per-device refreshRate are excluded from the batch.</small>
|
|
168
|
+
</div>
|
|
169
|
+
<div class="form-group">
|
|
170
|
+
<label for="matterBatchRefreshRate">OpenAPI Batch Polling Interval (seconds)</label>
|
|
171
|
+
<input type="number" id="matterBatchRefreshRate" min="30" max="86400" step="1" value="300" />
|
|
172
|
+
<small>Interval for batched OpenAPI polling. Falls back to OpenAPI Polling Interval if not set. Default: 300.</small>
|
|
173
|
+
</div>
|
|
174
|
+
<div class="form-group">
|
|
175
|
+
<label for="dailyApiLimit">OpenAPI Daily Request Limit</label>
|
|
176
|
+
<input type="number" id="dailyApiLimit" min="1000" max="100000" step="1" value="10000" />
|
|
177
|
+
<small>Maximum OpenAPI requests per day allowed by the plugin. Default: 10000.</small>
|
|
178
|
+
</div>
|
|
179
|
+
<div class="form-group">
|
|
180
|
+
<label for="dailyApiReserveForCommands">OpenAPI Reserve for Commands</label>
|
|
181
|
+
<input type="number" id="dailyApiReserveForCommands" min="0" max="10000" step="1" value="1000" />
|
|
182
|
+
<small>Requests reserved for user actions. When remaining budget reaches this value, background polling pauses. Default: 1000.</small>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="form-group">
|
|
185
|
+
<label for="dailyApiResetLocalMidnight">
|
|
186
|
+
<input type="checkbox" id="dailyApiResetLocalMidnight" /> Reset OpenAPI Counter at Local Midnight
|
|
187
|
+
</label>
|
|
188
|
+
<small>If true, resets the daily OpenAPI request counter at local midnight. If false, resets at UTC midnight.</small>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="form-group">
|
|
191
|
+
<label for="webhookOnlyOnReserve">
|
|
192
|
+
<input type="checkbox" id="webhookOnlyOnReserve" /> Only Allow Webhooks on Reserve
|
|
193
|
+
</label>
|
|
194
|
+
<small>When remaining OpenAPI budget reaches the reserve, only webhooks and user commands are allowed. Background polling/discovery pauses.</small>
|
|
195
|
+
</div>
|
|
196
|
+
<div class="form-group">
|
|
197
|
+
<label for="matterBatchConcurrency">OpenAPI Batch Concurrency</label>
|
|
198
|
+
<input type="number" id="matterBatchConcurrency" min="1" max="20" step="1" value="5" />
|
|
199
|
+
<small>Maximum number of parallel OpenAPI status calls during a batch. Default: 5.</small>
|
|
200
|
+
</div>
|
|
201
|
+
<div class="form-group">
|
|
202
|
+
<label for="matterBatchJitter">OpenAPI Batch Jitter (seconds)</label>
|
|
203
|
+
<input type="number" id="matterBatchJitter" min="0" max="300" step="1" value="0" />
|
|
204
|
+
<small>Random startup delay before the first batch to reduce synchronized spikes. Default: 0.</small>
|
|
205
|
+
</div>
|
|
206
|
+
<button type="button" id="saveAdvancedSettingsBtn">Save Advanced Settings</button>
|
|
207
|
+
<div id="advancedSettingsStatus" style="margin-top: 8px; font-size: 12px;"></div>
|
|
208
|
+
</form>
|
|
209
|
+
</div>
|
|
144
210
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const resp = await homebridge.request('/credentials', {})
|
|
149
|
-
console.log('Load credentials response:', resp)
|
|
150
|
-
|
|
151
|
-
if (!resp || resp.success === false) {
|
|
152
|
-
console.error('Failed to load credentials:', resp)
|
|
153
|
-
return
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const creds = resp.data || {}
|
|
157
|
-
const tokenStatus = document.getElementById('tokenStatus')
|
|
158
|
-
const secretStatus = document.getElementById('secretStatus')
|
|
159
|
-
|
|
160
|
-
if (creds.hasToken) {
|
|
161
|
-
tokenStatus.textContent = `✓ Configured (${creds.tokenLength} characters)`
|
|
162
|
-
tokenStatus.classList.add('ok')
|
|
163
|
-
} else {
|
|
164
|
-
tokenStatus.textContent = 'Not configured'
|
|
165
|
-
tokenStatus.classList.remove('ok')
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (creds.hasSecret) {
|
|
169
|
-
secretStatus.textContent = `✓ Configured (${creds.secretLength} characters)`
|
|
170
|
-
secretStatus.classList.add('ok')
|
|
171
|
-
} else {
|
|
172
|
-
secretStatus.textContent = 'Not configured'
|
|
173
|
-
secretStatus.classList.remove('ok')
|
|
174
|
-
}
|
|
175
|
-
} catch (e) {
|
|
176
|
-
console.error('Error loading credentials:', e)
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
async function saveCredentials() {
|
|
181
|
-
const token = document.getElementById('token').value
|
|
182
|
-
const secret = document.getElementById('secret').value
|
|
183
|
-
const saveStatus = document.getElementById('saveStatus')
|
|
184
|
-
const saveBtn = document.getElementById('saveBtn')
|
|
185
|
-
|
|
186
|
-
if (!token || !secret) {
|
|
187
|
-
saveStatus.textContent = 'Please enter both token and secret'
|
|
188
|
-
saveStatus.classList.add('error')
|
|
189
|
-
return
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
try {
|
|
193
|
-
saveBtn.disabled = true
|
|
194
|
-
saveBtn.textContent = 'Saving...'
|
|
195
|
-
|
|
196
|
-
console.log('Saving credentials...')
|
|
197
|
-
const resp = await homebridge.request('/credentials', { token, secret })
|
|
198
|
-
console.log('Save response:', resp)
|
|
199
|
-
|
|
200
|
-
if (!resp || resp.success === false) {
|
|
201
|
-
throw new Error(resp?.message || 'Save failed')
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const result = resp.data || resp
|
|
205
|
-
saveStatus.textContent = '✓ ' + (result.message || 'Credentials saved successfully')
|
|
206
|
-
saveStatus.classList.remove('error')
|
|
207
|
-
saveStatus.classList.add('success-msg')
|
|
208
|
-
|
|
209
|
-
// Clear inputs after successful save
|
|
210
|
-
document.getElementById('token').value = ''
|
|
211
|
-
document.getElementById('secret').value = ''
|
|
212
|
-
|
|
213
|
-
// Reload status to verify save
|
|
214
|
-
setTimeout(() => loadCredentialStatus(), 500)
|
|
215
|
-
|
|
216
|
-
// Clear status message
|
|
217
|
-
setTimeout(() => {
|
|
218
|
-
saveStatus.textContent = ''
|
|
219
|
-
saveStatus.classList.remove('success-msg')
|
|
220
|
-
}, 3000)
|
|
221
|
-
} catch (e) {
|
|
222
|
-
console.error('Save error:', e)
|
|
223
|
-
saveStatus.textContent = 'Error: ' + (e?.message || 'Failed to save')
|
|
224
|
-
saveStatus.classList.add('error')
|
|
225
|
-
} finally {
|
|
226
|
-
saveBtn.disabled = false
|
|
227
|
-
saveBtn.textContent = 'Save Credentials'
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async function fetchDevices() {
|
|
232
|
-
try {
|
|
233
|
-
const resp = await homebridge.request('/devices', {})
|
|
234
|
-
if (!resp || resp.success === false) throw new Error(resp?.data?.message || 'request failed')
|
|
235
|
-
return resp.data || []
|
|
236
|
-
} catch (e) {
|
|
237
|
-
console.error(e)
|
|
238
|
-
return []
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
async function discoverDevices() {
|
|
243
|
-
const btn = document.getElementById('discoverBtn')
|
|
244
|
-
const status = document.getElementById('discoverStatus')
|
|
245
|
-
const list = document.getElementById('discoveredList')
|
|
246
|
-
const ul = document.getElementById('discoveredDevices')
|
|
247
|
-
const autoAddAll = document.getElementById('autoAddAllCheckbox').checked
|
|
248
|
-
|
|
249
|
-
try {
|
|
250
|
-
btn.disabled = true
|
|
251
|
-
btn.textContent = '🔍 Discovering...'
|
|
252
|
-
status.textContent = 'Searching SwitchBot account...'
|
|
253
|
-
status.classList.remove('error')
|
|
254
|
-
|
|
255
|
-
const resp = await homebridge.request('/discover', {})
|
|
256
|
-
console.log('Discover response:', resp)
|
|
257
|
-
|
|
258
|
-
if (!resp || resp.success === false) {
|
|
259
|
-
throw new Error(resp?.data?.message || 'Discovery failed')
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const devicesRaw = resp.data || []
|
|
263
|
-
const devices = devicesRaw.filter((d, index, arr) =>
|
|
264
|
-
!!d?.id && arr.findIndex((x) => x?.id === d.id) === index,
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
if (!devices.length) {
|
|
268
|
-
status.textContent = 'No devices found in your SwitchBot account'
|
|
269
|
-
list.style.display = 'none'
|
|
270
|
-
return
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// If auto-add is enabled, add all devices immediately using bulk endpoint
|
|
274
|
-
if (autoAddAll) {
|
|
275
|
-
status.textContent = `Auto-adding ${devices.length} device(s)...`
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
const bulkResult = await homebridge.request('/add-devices', {
|
|
279
|
-
devices: devices.map(d => ({
|
|
280
|
-
deviceId: d.id,
|
|
281
|
-
name: d.name,
|
|
282
|
-
type: d.type,
|
|
283
|
-
}))
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
console.log('Bulk add response:', bulkResult)
|
|
287
|
-
|
|
288
|
-
if (!bulkResult || bulkResult.success === false) {
|
|
289
|
-
throw new Error(bulkResult?.data?.message || 'Bulk add failed')
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const addedCount = bulkResult?.data?.addedCount || 0
|
|
293
|
-
const skippedCount = bulkResult?.data?.skippedCount || 0
|
|
294
|
-
status.textContent = `✓ Added ${addedCount} device(s)` + (skippedCount > 0 ? ` (${skippedCount} skipped)` : '')
|
|
295
|
-
status.classList.remove('error')
|
|
296
|
-
list.style.display = 'none'
|
|
297
|
-
} catch (e) {
|
|
298
|
-
console.error('Bulk add error:', e)
|
|
299
|
-
status.textContent = '✗ Error: ' + (e?.message || 'Failed to add devices')
|
|
300
|
-
status.classList.add('error')
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Refresh the configured devices list
|
|
304
|
-
await loadConfiguredDevices()
|
|
305
|
-
return
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
status.textContent = `Found ${devices.length} device(s)`
|
|
309
|
-
ul.innerHTML = ''
|
|
310
|
-
|
|
311
|
-
for (const d of devices) {
|
|
312
|
-
const li = document.createElement('li')
|
|
313
|
-
li.className = 'device-item'
|
|
314
|
-
|
|
315
|
-
const info = document.createElement('div')
|
|
316
|
-
info.style.flex = '1'
|
|
317
|
-
const name = document.createElement('div')
|
|
318
|
-
name.style.fontWeight = '500'
|
|
319
|
-
name.textContent = d.name || d.id
|
|
320
|
-
const details = document.createElement('div')
|
|
321
|
-
details.style.fontSize = '12px'
|
|
322
|
-
details.style.opacity = '0.75'
|
|
323
|
-
details.textContent = `ID: ${d.id} | Type: ${d.type} | Model: ${d.model || 'N/A'}`
|
|
324
|
-
info.appendChild(name)
|
|
325
|
-
info.appendChild(details)
|
|
326
|
-
|
|
327
|
-
const addBtn = document.createElement('button')
|
|
328
|
-
addBtn.textContent = '+ Add'
|
|
329
|
-
addBtn.style.marginLeft = '8px'
|
|
330
|
-
addBtn.onclick = async () => {
|
|
331
|
-
await addDeviceToConfig(d)
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
li.appendChild(info)
|
|
335
|
-
li.appendChild(addBtn)
|
|
336
|
-
ul.appendChild(li)
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
list.style.display = 'block'
|
|
340
|
-
} catch (e) {
|
|
341
|
-
console.error('Discovery error:', e)
|
|
342
|
-
status.textContent = 'Error: ' + (e?.message || 'Discovery failed')
|
|
343
|
-
status.classList.add('error')
|
|
344
|
-
list.style.display = 'none'
|
|
345
|
-
} finally {
|
|
346
|
-
btn.disabled = false
|
|
347
|
-
btn.textContent = '🔍 Discover Devices'
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
async function addDeviceToConfig(device, options = {}) {
|
|
352
|
-
const { refresh = true, showStatus = true } = options
|
|
353
|
-
try {
|
|
354
|
-
console.log('Adding device to config:', device)
|
|
355
|
-
|
|
356
|
-
const resp = await homebridge.request('/add-device', {
|
|
357
|
-
deviceId: device.id,
|
|
358
|
-
name: device.name,
|
|
359
|
-
type: device.type,
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
console.log('Add device response:', resp)
|
|
363
|
-
|
|
364
|
-
if (!resp || resp.success === false) {
|
|
365
|
-
throw new Error(resp?.data?.message || 'Failed to add device')
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const alreadyExists = !!resp.data?.alreadyExists
|
|
369
|
-
const message = resp.data?.message || (alreadyExists
|
|
370
|
-
? `Device "${device.name}" already in config`
|
|
371
|
-
: `Device "${device.name}" added successfully!`)
|
|
372
|
-
|
|
373
|
-
if (showStatus) {
|
|
374
|
-
const status = document.getElementById('discoverStatus')
|
|
375
|
-
status.textContent = (alreadyExists ? '• ' : '✓ ') + message
|
|
376
|
-
status.classList.remove('error')
|
|
377
|
-
status.classList.add('success-msg')
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (refresh) {
|
|
381
|
-
await loadConfiguredDevices()
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
return { added: !alreadyExists }
|
|
385
|
-
} catch (e) {
|
|
386
|
-
console.error('Add device error:', e)
|
|
387
|
-
const status = document.getElementById('discoverStatus')
|
|
388
|
-
status.textContent = '✗ Error: ' + (e?.message || 'Failed to add device')
|
|
389
|
-
status.classList.add('error')
|
|
390
|
-
return { added: false }
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
async function loadConfiguredDevices() {
|
|
395
|
-
const list = await fetchDevices()
|
|
396
|
-
render(list)
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
async function deleteDevice(deviceId, deviceName) {
|
|
400
|
-
if (!confirm(`Are you sure you want to remove "${deviceName || deviceId}"?`)) {
|
|
401
|
-
return
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
try {
|
|
405
|
-
const resp = await homebridge.request('/delete-device', { deviceId })
|
|
406
|
-
|
|
407
|
-
if (!resp || resp.success === false) {
|
|
408
|
-
throw new Error(resp?.data?.message || 'Failed to delete device')
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// Refresh device list
|
|
412
|
-
const list = await fetchDevices()
|
|
413
|
-
render(list)
|
|
414
|
-
} catch (e) {
|
|
415
|
-
console.error('Delete error:', e)
|
|
416
|
-
alert('Error: ' + (e?.message || 'Failed to delete device'))
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const DEVICE_TYPES = [
|
|
421
|
-
'bot', 'curtain', 'fan', 'light', 'lightstrip', 'motion', 'contact',
|
|
422
|
-
'vacuum', 'lock', 'humidifier', 'temperature', 'relay', 'plug',
|
|
423
|
-
'wosweeper', 'blindtilt', 'curtain3', 'rollershade', 'meter',
|
|
424
|
-
'waterdetector', 'walletfinder', 'unknown'
|
|
425
|
-
]
|
|
426
|
-
|
|
427
|
-
async function editDevice(device) {
|
|
428
|
-
// Create modal dialog for editing
|
|
429
|
-
const div = document.createElement('div')
|
|
430
|
-
div.style.position = 'fixed'
|
|
431
|
-
div.style.top = '0'
|
|
432
|
-
div.style.left = '0'
|
|
433
|
-
div.style.width = '100%'
|
|
434
|
-
div.style.height = '100%'
|
|
435
|
-
div.style.background = 'rgba(0,0,0,0.7)'
|
|
436
|
-
div.style.display = 'flex'
|
|
437
|
-
div.style.alignItems = 'center'
|
|
438
|
-
div.style.justifyContent = 'center'
|
|
439
|
-
div.style.zIndex = '9999'
|
|
440
|
-
|
|
441
|
-
const modal = document.createElement('div')
|
|
442
|
-
modal.style.background = getComputedStyle(document.body).backgroundColor
|
|
443
|
-
modal.style.color = getComputedStyle(document.body).color
|
|
444
|
-
modal.style.padding = '24px'
|
|
445
|
-
modal.style.borderRadius = '8px'
|
|
446
|
-
modal.style.minWidth = '400px'
|
|
447
|
-
modal.style.boxShadow = '0 10px 40px rgba(0,0,0,0.3)'
|
|
448
|
-
|
|
449
|
-
const title = document.createElement('h3')
|
|
450
|
-
title.textContent = 'Edit Device'
|
|
451
|
-
title.style.marginTop = '0'
|
|
452
|
-
|
|
453
|
-
const nameLabel = document.createElement('label')
|
|
454
|
-
nameLabel.textContent = 'Device Name'
|
|
455
|
-
nameLabel.style.display = 'block'
|
|
456
|
-
nameLabel.style.marginBottom = '8px'
|
|
457
|
-
nameLabel.style.fontWeight = '500'
|
|
458
|
-
|
|
459
|
-
const nameInput = document.createElement('input')
|
|
460
|
-
nameInput.type = 'text'
|
|
461
|
-
nameInput.value = device.name || device.id
|
|
462
|
-
nameInput.style.width = '100%'
|
|
463
|
-
nameInput.style.marginBottom = '16px'
|
|
464
|
-
|
|
465
|
-
const typeLabel = document.createElement('label')
|
|
466
|
-
typeLabel.textContent = 'Device Type'
|
|
467
|
-
typeLabel.style.display = 'block'
|
|
468
|
-
typeLabel.style.marginBottom = '8px'
|
|
469
|
-
typeLabel.style.fontWeight = '500'
|
|
470
|
-
|
|
471
|
-
const typeSelect = document.createElement('select')
|
|
472
|
-
typeSelect.style.width = '100%'
|
|
473
|
-
typeSelect.style.padding = '8px'
|
|
474
|
-
typeSelect.style.marginBottom = '16px'
|
|
475
|
-
typeSelect.style.borderRadius = '4px'
|
|
476
|
-
typeSelect.style.background = getComputedStyle(nameInput).background
|
|
477
|
-
typeSelect.style.color = getComputedStyle(nameInput).color
|
|
478
|
-
typeSelect.style.border = getComputedStyle(nameInput).border
|
|
479
|
-
|
|
480
|
-
DEVICE_TYPES.forEach(t => {
|
|
481
|
-
const opt = document.createElement('option')
|
|
482
|
-
opt.value = t
|
|
483
|
-
opt.text = t
|
|
484
|
-
opt.selected = (device.type || 'unknown') === t
|
|
485
|
-
typeSelect.appendChild(opt)
|
|
486
|
-
})
|
|
487
|
-
|
|
488
|
-
const buttons = document.createElement('div')
|
|
489
|
-
buttons.style.display = 'flex'
|
|
490
|
-
buttons.style.gap = '8px'
|
|
491
|
-
buttons.style.justifyContent = 'flex-end'
|
|
492
|
-
|
|
493
|
-
const cancelBtn = document.createElement('button')
|
|
494
|
-
cancelBtn.textContent = 'Cancel'
|
|
495
|
-
cancelBtn.style.background = '#666'
|
|
496
|
-
cancelBtn.onclick = () => div.remove()
|
|
497
|
-
|
|
498
|
-
const saveBtn = document.createElement('button')
|
|
499
|
-
saveBtn.textContent = 'Save'
|
|
500
|
-
saveBtn.onclick = async () => {
|
|
501
|
-
try {
|
|
502
|
-
const params = {
|
|
503
|
-
deviceId: device.id,
|
|
504
|
-
configDeviceName: nameInput.value || undefined,
|
|
505
|
-
configDeviceType: typeSelect.value,
|
|
506
|
-
}
|
|
507
|
-
console.log('[Edit Device] Sending update request with params:', params)
|
|
508
|
-
|
|
509
|
-
const resp = await homebridge.request('/update-device', params)
|
|
510
|
-
|
|
511
|
-
console.log('[Edit Device] Update response:', resp)
|
|
512
|
-
|
|
513
|
-
if (!resp || resp.success === false) {
|
|
514
|
-
throw new Error(resp?.data?.message || 'Failed to update device')
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Refresh device list
|
|
518
|
-
console.log('[Edit Device] Refreshing device list after update')
|
|
519
|
-
const list = await fetchDevices()
|
|
520
|
-
render(list)
|
|
521
|
-
div.remove()
|
|
522
|
-
} catch (e) {
|
|
523
|
-
console.error('Update error:', e)
|
|
524
|
-
alert('Error: ' + (e?.message || 'Failed to update device'))
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
buttons.appendChild(cancelBtn)
|
|
529
|
-
buttons.appendChild(saveBtn)
|
|
530
|
-
|
|
531
|
-
modal.appendChild(title)
|
|
532
|
-
modal.appendChild(nameLabel)
|
|
533
|
-
modal.appendChild(nameInput)
|
|
534
|
-
modal.appendChild(typeLabel)
|
|
535
|
-
modal.appendChild(typeSelect)
|
|
536
|
-
modal.appendChild(buttons)
|
|
537
|
-
|
|
538
|
-
div.appendChild(modal)
|
|
539
|
-
document.body.appendChild(div)
|
|
540
|
-
nameInput.focus()
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
function render(list) {
|
|
544
|
-
const ul = document.getElementById('devices')
|
|
545
|
-
const status = document.getElementById('status')
|
|
546
|
-
if (!list.length) {
|
|
547
|
-
status.textContent = 'No devices found in config.'
|
|
548
|
-
ul.innerHTML = ''
|
|
549
|
-
return
|
|
550
|
-
}
|
|
551
|
-
status.textContent = `Found ${list.length} device(s)`
|
|
552
|
-
ul.innerHTML = ''
|
|
553
|
-
for (const d of list) {
|
|
554
|
-
const li = document.createElement('li')
|
|
555
|
-
li.className = 'device-item'
|
|
556
|
-
|
|
557
|
-
const info = document.createElement('div')
|
|
558
|
-
info.style.flex = '1'
|
|
559
|
-
|
|
560
|
-
const name = document.createElement('div')
|
|
561
|
-
name.style.fontWeight = '500'
|
|
562
|
-
name.textContent = d.name || d.id
|
|
563
|
-
|
|
564
|
-
const code = document.createElement('code')
|
|
565
|
-
code.textContent = d.id
|
|
566
|
-
code.style.fontSize = '12px'
|
|
567
|
-
code.style.opacity = '0.75'
|
|
568
|
-
code.style.marginLeft = '8px'
|
|
569
|
-
|
|
570
|
-
const meta = document.createElement('div')
|
|
571
|
-
meta.style.opacity = '0.75'
|
|
572
|
-
meta.style.marginTop = '4px'
|
|
573
|
-
meta.style.fontSize = '12px'
|
|
574
|
-
const typeText = d.type ? `type: ${d.type}` : ''
|
|
575
|
-
const connText = d.connectionPreference ? `conn: ${d.connectionPreference}` : ''
|
|
576
|
-
const roomText = d.room ? `room: ${d.room}` : ''
|
|
577
|
-
meta.textContent = [typeText, connText, roomText].filter(Boolean).join(' | ')
|
|
578
|
-
|
|
579
|
-
info.appendChild(name)
|
|
580
|
-
info.appendChild(code)
|
|
581
|
-
info.appendChild(meta)
|
|
582
|
-
|
|
583
|
-
const buttons = document.createElement('div')
|
|
584
|
-
buttons.style.display = 'flex'
|
|
585
|
-
buttons.style.gap = '8px'
|
|
586
|
-
|
|
587
|
-
const editBtn = document.createElement('button')
|
|
588
|
-
editBtn.textContent = '✏️ Edit'
|
|
589
|
-
editBtn.style.padding = '6px 12px'
|
|
590
|
-
editBtn.style.fontSize = '12px'
|
|
591
|
-
editBtn.onclick = async () => {
|
|
592
|
-
await editDevice(d)
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
const copyBtn = document.createElement('button')
|
|
596
|
-
copyBtn.textContent = 'Copy ID'
|
|
597
|
-
copyBtn.style.padding = '6px 12px'
|
|
598
|
-
copyBtn.style.fontSize = '12px'
|
|
599
|
-
copyBtn.addEventListener('click', async () => {
|
|
600
|
-
try {
|
|
601
|
-
await navigator.clipboard.writeText(d.id)
|
|
602
|
-
copyBtn.textContent = 'Copied'
|
|
603
|
-
copyBtn.classList.add('success')
|
|
604
|
-
setTimeout(() => {
|
|
605
|
-
copyBtn.textContent = 'Copy ID'
|
|
606
|
-
copyBtn.classList.remove('success')
|
|
607
|
-
}, 1200)
|
|
608
|
-
} catch (e) {
|
|
609
|
-
alert('Failed to copy')
|
|
610
|
-
}
|
|
611
|
-
})
|
|
612
|
-
|
|
613
|
-
const deleteBtn = document.createElement('button')
|
|
614
|
-
deleteBtn.textContent = '🗑️ Delete'
|
|
615
|
-
deleteBtn.style.padding = '6px 12px'
|
|
616
|
-
deleteBtn.style.fontSize = '12px'
|
|
617
|
-
deleteBtn.style.background = '#ef4444'
|
|
618
|
-
deleteBtn.onclick = async () => {
|
|
619
|
-
await deleteDevice(d.id, d.name || d.id)
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
buttons.appendChild(editBtn)
|
|
623
|
-
buttons.appendChild(copyBtn)
|
|
624
|
-
buttons.appendChild(deleteBtn)
|
|
625
|
-
|
|
626
|
-
li.appendChild(info)
|
|
627
|
-
li.appendChild(buttons)
|
|
628
|
-
ul.appendChild(li)
|
|
629
|
-
}
|
|
630
|
-
}
|
|
211
|
+
<script type="module" src="js/app.js"></script>
|
|
212
|
+
<script type="module" src="js/advanced-settings.js"></script>
|
|
213
|
+
</body>
|
|
631
214
|
|
|
632
|
-
|
|
633
|
-
await loadCredentialStatus()
|
|
634
|
-
const list = await fetchDevices()
|
|
635
|
-
render(list)
|
|
636
|
-
})()
|
|
637
|
-
</script>
|
|
638
|
-
</body>
|
|
639
|
-
</html>
|
|
215
|
+
</html>
|