Skip to content

Commit d44768c

Browse files
author
homebridge-bot
committed
UI
1 parent 5491d4a commit d44768c

File tree

2 files changed

+200
-10
lines changed

2 files changed

+200
-10
lines changed

src/homebridge-ui/public/index.html

Lines changed: 129 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,135 @@
55
<meta name="viewport" content="width=device-width,initial-scale=1" />
66
<title>SwitchBot Plugin UI</title>
77
<style>
8-
body { font-family: system-ui, -apple-system, Arial; padding: 16px; }
9-
h1 { font-size: 18px }
8+
body { font-family: system-ui, -apple-system, Arial; padding: 16px; background:#1a1a1a; color:#fff }
9+
h1 { font-size: 24px; margin-top:0 }
10+
h2 { font-size: 16px; margin-top: 24px; margin-bottom: 12px; border-bottom:1px solid #444; padding-bottom:8px }
1011
ul { padding-left: 0; list-style: none }
1112
li { margin: 8px 0; display:flex; gap:8px; align-items:center }
12-
button { padding: 6px 10px }
13-
code { background:#f3f3f3; padding:4px 6px; border-radius:4px }
13+
button { padding: 8px 16px; background:#6366f1; color:#fff; border:none; border-radius:4px; cursor:pointer }
14+
button:hover { background:#4f46e5 }
15+
button.success { background:#10b981 }
16+
code { background:#333; padding:4px 6px; border-radius:4px; color:#fff }
17+
.form-group { margin-bottom:16px }
18+
label { display:block; margin-bottom:6px; font-weight:500 }
19+
input { width:100%; max-width:400px; padding:8px; background:#2a2a2a; border:1px solid #444; border-radius:4px; color:#fff; font-family:monospace }
20+
input:focus { outline:none; border-color:#6366f1 }
21+
.status { font-size:14px; color:#888; margin-top:4px }
22+
.status.ok { color:#10b981 }
23+
.error { color:#ef4444 }
24+
.success-msg { color:#10b981; margin-top:8px }
25+
.card { background:#2a2a2a; padding:16px; border-radius:8px; margin-bottom:16px }
1426
</style>
1527
</head>
1628
<body>
17-
<h1>SwitchBot: Configured Devices</h1>
18-
<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>
19-
<div id="status">Loading…</div>
20-
<ul id="devices"></ul>
29+
<h1>🤖 SwitchBot Configuration</h1>
30+
31+
<div class="card">
32+
<h2>API Credentials</h2>
33+
<p>Configure your SwitchBot API token and secret to enable device discovery and control.</p>
34+
35+
<div class="form-group">
36+
<label for="token">API Token:</label>
37+
<input type="password" id="token" placeholder="Enter your SwitchBot API token" />
38+
<div class="status" id="tokenStatus"></div>
39+
</div>
40+
41+
<div class="form-group">
42+
<label for="secret">API Secret:</label>
43+
<input type="password" id="secret" placeholder="Enter your SwitchBot API secret" />
44+
<div class="status" id="secretStatus"></div>
45+
</div>
46+
47+
<button id="saveBtn" onclick="saveCredentials()">Save Credentials</button>
48+
<div id="saveStatus"></div>
49+
</div>
50+
51+
<div class="card">
52+
<h2>Configured Devices</h2>
53+
<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>
54+
<div id="status">Loading…</div>
55+
<ul id="devices"></ul>
56+
</div>
2157

2258
<script>
59+
async function loadCredentialStatus() {
60+
try {
61+
const resp = await homebridge.request('/credentials', {})
62+
if (!resp || !resp.success === false) {
63+
if (!resp?.data) {
64+
console.error('Failed to load credentials:', resp?.data?.message)
65+
return
66+
}
67+
}
68+
69+
const creds = resp.data || {}
70+
const tokenStatus = document.getElementById('tokenStatus')
71+
const secretStatus = document.getElementById('secretStatus')
72+
73+
if (creds.hasToken) {
74+
tokenStatus.textContent = `✓ Configured (${creds.tokenLength} characters)`
75+
tokenStatus.classList.add('ok')
76+
} else {
77+
tokenStatus.textContent = 'Not configured'
78+
}
79+
80+
if (creds.hasSecret) {
81+
secretStatus.textContent = `✓ Configured (${creds.secretLength} characters)`
82+
secretStatus.classList.add('ok')
83+
} else {
84+
secretStatus.textContent = 'Not configured'
85+
}
86+
} catch (e) {
87+
console.error('Error loading credentials:', e)
88+
}
89+
}
90+
91+
async function saveCredentials() {
92+
const token = document.getElementById('token').value
93+
const secret = document.getElementById('secret').value
94+
const saveStatus = document.getElementById('saveStatus')
95+
const saveBtn = document.getElementById('saveBtn')
96+
97+
if (!token || !secret) {
98+
saveStatus.textContent = 'Please enter both token and secret'
99+
saveStatus.classList.add('error')
100+
return
101+
}
102+
103+
try {
104+
saveBtn.disabled = true
105+
saveBtn.textContent = 'Saving...'
106+
107+
const resp = await homebridge.request('/credentials', { token, secret })
108+
if (!resp || resp.success === false) {
109+
throw new Error(resp?.data?.message || 'Save failed')
110+
}
111+
112+
saveStatus.textContent = '✓ ' + (resp.data?.message || 'Credentials saved')
113+
saveStatus.classList.remove('error')
114+
saveStatus.classList.add('success-msg')
115+
116+
// Clear inputs after successful save
117+
document.getElementById('token').value = ''
118+
document.getElementById('secret').value = ''
119+
120+
// Reload status
121+
setTimeout(() => loadCredentialStatus(), 1000)
122+
123+
// Clear status message
124+
setTimeout(() => {
125+
saveStatus.textContent = ''
126+
saveStatus.classList.remove('success-msg')
127+
}, 3000)
128+
} catch (e) {
129+
saveStatus.textContent = 'Error: ' + (e?.message || 'Failed to save')
130+
saveStatus.classList.add('error')
131+
} finally {
132+
saveBtn.disabled = false
133+
saveBtn.textContent = 'Save Credentials'
134+
}
135+
}
136+
23137
async function fetchDevices() {
24138
try {
25139
const resp = await homebridge.request('/devices', {})
@@ -59,7 +173,11 @@ <h1>SwitchBot: Configured Devices</h1>
59173
try {
60174
await navigator.clipboard.writeText(d.id)
61175
btn.textContent = 'Copied'
62-
setTimeout(() => (btn.textContent = 'Copy ID'), 1200)
176+
btn.classList.add('success')
177+
setTimeout(() => {
178+
btn.textContent = 'Copy ID'
179+
btn.classList.remove('success')
180+
}, 1200)
63181
} catch (e) {
64182
alert('Failed to copy')
65183
}
@@ -73,9 +191,10 @@ <h1>SwitchBot: Configured Devices</h1>
73191
}
74192

75193
(async () => {
194+
await loadCredentialStatus()
76195
const list = await fetchDevices()
77196
render(list)
78197
})()
79198
</script>
80199
</body>
81-
</html>
200+
</html>

src/homebridge-ui/server.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,28 @@ import { HomebridgePluginUiServer, RequestError } from '@homebridge/plugin-ui-ut
44

55
const server = new HomebridgePluginUiServer()
66

7+
// Helper function to find the SwitchBot platform config
8+
async function getSwitchBotPlatformConfig() {
9+
const cfgPath = server.homebridgeConfigPath
10+
if (!cfgPath) {
11+
throw new Error('HOMEBRIDGE_CONFIG_PATH not set')
12+
}
13+
14+
const raw = await fs.readFile(cfgPath, 'utf8')
15+
const cfg = JSON.parse(raw)
16+
const platforms = Array.isArray(cfg.platforms) ? cfg.platforms : []
17+
18+
for (const p of platforms) {
19+
const platformName = p.platform || p.name || ''
20+
if (!platformName || !/switchbot/i.test(String(platformName))) {
21+
continue
22+
}
23+
return { config: cfg, platform: p, cfgPath }
24+
}
25+
26+
throw new Error('SwitchBot platform not found in config')
27+
}
28+
729
server.onRequest('/devices', async () => {
830
try {
931
const cfgPath = server.homebridgeConfigPath
@@ -51,6 +73,55 @@ server.onRequest('/devices', async () => {
5173
}
5274
})
5375

76+
server.onRequest('/credentials', async (body: any) => {
77+
try {
78+
// Handle both GET and POST requests
79+
if (!body || Object.keys(body).length === 0) {
80+
// GET request - return current status
81+
const { platform } = await getSwitchBotPlatformConfig()
82+
const options = platform.options || platform
83+
84+
return {
85+
hasToken: !!options.token,
86+
hasSecret: !!options.secret,
87+
tokenLength: options.token ? String(options.token).length : 0,
88+
secretLength: options.secret ? String(options.secret).length : 0,
89+
}
90+
} else {
91+
// POST request - save credentials
92+
const { token, secret } = body
93+
94+
if (!token || !secret) {
95+
throw new Error('Token and secret are required')
96+
}
97+
98+
const { config, platform, cfgPath } = await getSwitchBotPlatformConfig()
99+
100+
// Update the platform config with new credentials
101+
const options = platform.options || {}
102+
options.token = token
103+
options.secret = secret
104+
105+
if (platform.options) {
106+
platform.options = options
107+
} else {
108+
platform.token = token
109+
platform.secret = secret
110+
}
111+
112+
// Write back to config file
113+
await fs.writeFile(cfgPath, JSON.stringify(config, null, 2), 'utf8')
114+
115+
return {
116+
success: true,
117+
message: 'Credentials saved successfully',
118+
}
119+
}
120+
} catch (e) {
121+
throw new RequestError('Failed to handle credentials request', e)
122+
}
123+
})
124+
54125
server.ready()
55126

56127
export default server

0 commit comments

Comments
 (0)