@switchbot/homebridge-switchbot 5.0.0-beta.112 → 5.0.0-beta.114
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/MIGRATION.md +10 -0
- package/README.md +43 -0
- package/TODO.md +263 -0
- package/config.schema.json +27 -10
- package/dist/device-types.d.ts.map +1 -1
- package/dist/device-types.js +93 -63
- package/dist/device-types.js.map +1 -1
- package/dist/deviceFactory.d.ts.map +1 -1
- package/dist/deviceFactory.js +55 -33
- package/dist/deviceFactory.js.map +1 -1
- package/dist/devices/genericDevice.d.ts +12 -1
- package/dist/devices/genericDevice.d.ts.map +1 -1
- package/dist/devices/genericDevice.js +121 -1
- package/dist/devices/genericDevice.js.map +1 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +51 -0
- package/dist/errors.js.map +1 -0
- package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/devices.js +80 -1
- package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
- package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/discovery.js +73 -41
- package/dist/homebridge-ui/endpoints/discovery.js.map +1 -1
- package/dist/homebridge-ui/public/css/styles.css +98 -0
- package/dist/homebridge-ui/public/index.html +57 -0
- package/dist/homebridge-ui/public/js/api.d.ts +24 -1
- package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/api.js +33 -2
- package/dist/homebridge-ui/public/js/api.js.map +1 -1
- package/dist/homebridge-ui/public/js/api.ts +52 -3
- package/dist/homebridge-ui/public/js/app.js +1560 -104
- package/dist/homebridge-ui/public/js/app.js.map +3 -3
- package/dist/homebridge-ui/public/js/app.ts +2 -1
- package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/devices.js +2 -0
- package/dist/homebridge-ui/public/js/devices.js.map +1 -1
- package/dist/homebridge-ui/public/js/devices.ts +13 -1
- package/dist/homebridge-ui/public/js/discovery.d.ts +1 -0
- package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.js +1223 -126
- package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.ts +1323 -145
- package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/modals.js +42 -0
- package/dist/homebridge-ui/public/js/modals.js.map +1 -1
- package/dist/homebridge-ui/public/js/modals.ts +50 -1
- package/dist/homebridge-ui/public/js/render.d.ts +3 -0
- package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/render.js +373 -1
- package/dist/homebridge-ui/public/js/render.js.map +1 -1
- package/dist/homebridge-ui/public/js/render.ts +405 -2
- package/dist/platform.d.ts +1 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +7 -3
- package/dist/platform.js.map +1 -1
- package/dist/switchbotClient.d.ts.map +1 -1
- package/dist/switchbotClient.js +51 -5
- package/dist/switchbotClient.js.map +1 -1
- package/docs/assets/highlight.css +9 -2
- package/docs/index.html +28 -2
- package/docs/variables/default.html +1 -1
- package/package.json +2 -2
- package/src/device-types.ts +95 -64
- package/src/deviceFactory.ts +55 -33
- package/src/devices/genericDevice.ts +113 -1
- package/src/errors.ts +43 -0
- package/src/homebridge-ui/endpoints/devices.ts +82 -1
- package/src/homebridge-ui/endpoints/discovery.ts +78 -44
- package/src/homebridge-ui/public/css/styles.css +98 -0
- package/src/homebridge-ui/public/index.html +57 -0
- package/src/homebridge-ui/public/js/api.ts +52 -3
- package/src/homebridge-ui/public/js/app.ts +2 -1
- package/src/homebridge-ui/public/js/devices.ts +13 -1
- package/src/homebridge-ui/public/js/discovery.ts +1323 -145
- package/src/homebridge-ui/public/js/modals.ts +50 -1
- package/src/homebridge-ui/public/js/render.ts +405 -2
- package/src/platform.ts +7 -3
- package/src/switchbotClient.ts +44 -9
- package/test/matter-integration.spec.ts +21 -0
- package/todo/COMPLETED.md +300 -0
- package/todo/TODO.md +80 -66
|
@@ -176,9 +176,299 @@ function scrollToConfiguredDevice(deviceId: string): void {
|
|
|
176
176
|
}, 1800)
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
function createConnectionTestControls(device: any): HTMLElement {
|
|
180
|
+
const controls = document.createElement('div')
|
|
181
|
+
controls.style.display = 'inline-flex'
|
|
182
|
+
controls.style.alignItems = 'center'
|
|
183
|
+
controls.style.gap = '6px'
|
|
184
|
+
|
|
185
|
+
const button = document.createElement('button')
|
|
186
|
+
button.textContent = 'Test Connection'
|
|
187
|
+
button.className = 'secondary'
|
|
188
|
+
button.style.padding = '4px 9px'
|
|
189
|
+
button.style.fontSize = '11px'
|
|
190
|
+
|
|
191
|
+
const status = document.createElement('span')
|
|
192
|
+
status.style.fontSize = '10px'
|
|
193
|
+
status.style.opacity = '0.85'
|
|
194
|
+
status.style.whiteSpace = 'normal'
|
|
195
|
+
status.style.overflowWrap = 'anywhere'
|
|
196
|
+
|
|
197
|
+
button.onclick = async () => {
|
|
198
|
+
const startedAt = Date.now()
|
|
199
|
+
button.disabled = true
|
|
200
|
+
button.textContent = 'Testing...'
|
|
201
|
+
status.textContent = 'Checking...'
|
|
202
|
+
status.style.color = '#6b7280'
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const { testDeviceConnection } = await import('./api.js')
|
|
206
|
+
const result = await testDeviceConnection({
|
|
207
|
+
deviceId: String(device?.id || device?.deviceId || ''),
|
|
208
|
+
connectionType: device?.connectionType,
|
|
209
|
+
address: device?.address,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const measuredLatency = Number(result?.latencyMs) > 0
|
|
213
|
+
? Number(result.latencyMs)
|
|
214
|
+
: Date.now() - startedAt
|
|
215
|
+
|
|
216
|
+
if (result?.success) {
|
|
217
|
+
const method = result?.method || 'Auto'
|
|
218
|
+
status.textContent = `✓ ${method} · ${measuredLatency}ms`
|
|
219
|
+
status.style.color = '#16a34a'
|
|
220
|
+
} else {
|
|
221
|
+
const detail = result?.message ? ` · ${result.message}` : ''
|
|
222
|
+
status.textContent = `✗ Failed · ${measuredLatency}ms${detail}`
|
|
223
|
+
status.style.color = '#dc2626'
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
status.textContent = `✗ Failed · ${Date.now() - startedAt}ms`
|
|
227
|
+
status.style.color = '#dc2626'
|
|
228
|
+
} finally {
|
|
229
|
+
button.disabled = false
|
|
230
|
+
button.textContent = 'Test Connection'
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
controls.appendChild(button)
|
|
235
|
+
controls.appendChild(status)
|
|
236
|
+
return controls
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function formatLastSeen(value: any): string {
|
|
240
|
+
if (!value) {
|
|
241
|
+
return 'N/A'
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const date = new Date(value)
|
|
245
|
+
return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString()
|
|
246
|
+
} catch (_e) {
|
|
247
|
+
return String(value)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function renderDeviceDetailsPanel(device: any): HTMLElement {
|
|
252
|
+
const details = document.createElement('div')
|
|
253
|
+
details.style.cssText = `
|
|
254
|
+
border-top: 1px solid #ddd;
|
|
255
|
+
padding: 8px;
|
|
256
|
+
background: #f9fafb;
|
|
257
|
+
border-radius: 4px;
|
|
258
|
+
font-size: 12px;
|
|
259
|
+
margin-top: 4px;
|
|
260
|
+
`
|
|
261
|
+
|
|
262
|
+
// --- Battery history trending ---
|
|
263
|
+
// Persist battery readings in localStorage per device
|
|
264
|
+
const batteryHistoryKey = `batteryHistory:${device?.id || device?.deviceId}`
|
|
265
|
+
let batteryHistory: Array<{ value: number, ts: number }> = []
|
|
266
|
+
try {
|
|
267
|
+
const raw = localStorage.getItem(batteryHistoryKey)
|
|
268
|
+
if (raw) batteryHistory = JSON.parse(raw)
|
|
269
|
+
} catch {}
|
|
270
|
+
const now = Date.now()
|
|
271
|
+
if (typeof device?.battery === 'number') {
|
|
272
|
+
// Only add if different from last or >1h since last
|
|
273
|
+
const last = batteryHistory[batteryHistory.length - 1]
|
|
274
|
+
if (!last || last.value !== device.battery || now - last.ts > 60 * 60 * 1000) {
|
|
275
|
+
batteryHistory.push({ value: device.battery, ts: now })
|
|
276
|
+
// Keep only last 30 entries (about a month if daily)
|
|
277
|
+
if (batteryHistory.length > 30) batteryHistory = batteryHistory.slice(-30)
|
|
278
|
+
try { localStorage.setItem(batteryHistoryKey, JSON.stringify(batteryHistory)) } catch {}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const rows: Array<{ label: string, value: string, copyable?: boolean }> = [
|
|
283
|
+
{ label: 'Device ID', value: String(device?.id || device?.deviceId || 'N/A'), copyable: !!(device?.id || device?.deviceId) },
|
|
284
|
+
{ label: 'MAC Address', value: String(device?.address || 'N/A'), copyable: !!device?.address },
|
|
285
|
+
{ label: 'Device Type', value: String(device?.type || device?.configDeviceType || 'N/A') },
|
|
286
|
+
{ label: 'Model', value: String(device?.model || 'N/A') },
|
|
287
|
+
{ label: 'Hub ID', value: String(device?.hubDeviceId || 'N/A') },
|
|
288
|
+
{ label: 'Battery', value: device?.battery !== undefined && device?.battery !== null ? `${device.battery}%` : 'N/A' },
|
|
289
|
+
{ label: 'Firmware', value: String(device?.version || device?.firmware || 'N/A') },
|
|
290
|
+
{ label: 'Cloud Service', value: device?.enabled === false ? 'Disabled' : 'Enabled' },
|
|
291
|
+
{ label: 'Last Seen', value: formatLastSeen(device?.lastSeen || device?.lastseen || device?.updatedAt) },
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
for (const row of rows) {
|
|
297
|
+
const line = document.createElement('div')
|
|
298
|
+
line.style.display = 'flex'
|
|
299
|
+
line.style.alignItems = 'center'
|
|
300
|
+
line.style.justifyContent = 'space-between'
|
|
301
|
+
line.style.gap = '8px'
|
|
302
|
+
line.style.padding = '2px 0'
|
|
303
|
+
|
|
304
|
+
const label = document.createElement('span')
|
|
305
|
+
label.style.fontWeight = '600'
|
|
306
|
+
label.style.minWidth = '110px'
|
|
307
|
+
label.textContent = `${row.label}:`
|
|
308
|
+
|
|
309
|
+
const valueWrap = document.createElement('span')
|
|
310
|
+
valueWrap.style.display = 'inline-flex'
|
|
311
|
+
valueWrap.style.alignItems = 'center'
|
|
312
|
+
valueWrap.style.gap = '6px'
|
|
313
|
+
valueWrap.style.flex = '1'
|
|
314
|
+
valueWrap.style.justifyContent = 'flex-end'
|
|
315
|
+
valueWrap.style.minWidth = '0'
|
|
316
|
+
|
|
317
|
+
const value = document.createElement('span')
|
|
318
|
+
value.style.fontFamily = 'monospace'
|
|
319
|
+
value.style.fontSize = '11px'
|
|
320
|
+
value.style.opacity = '0.9'
|
|
321
|
+
value.style.whiteSpace = 'normal'
|
|
322
|
+
value.style.overflowWrap = 'anywhere'
|
|
323
|
+
value.style.wordBreak = 'break-word'
|
|
324
|
+
value.style.textAlign = 'right'
|
|
325
|
+
value.textContent = row.value
|
|
326
|
+
|
|
327
|
+
valueWrap.appendChild(value)
|
|
328
|
+
|
|
329
|
+
if (row.copyable && row.value && row.value !== 'N/A') {
|
|
330
|
+
const copyBtn = document.createElement('button')
|
|
331
|
+
copyBtn.textContent = '📋'
|
|
332
|
+
copyBtn.title = `Copy ${row.label}`
|
|
333
|
+
copyBtn.style.padding = '2px 6px'
|
|
334
|
+
copyBtn.style.fontSize = '10px'
|
|
335
|
+
copyBtn.style.lineHeight = '1'
|
|
336
|
+
copyBtn.style.background = '#e5e7eb'
|
|
337
|
+
copyBtn.style.color = '#111827'
|
|
338
|
+
|
|
339
|
+
copyBtn.onclick = async () => {
|
|
340
|
+
try {
|
|
341
|
+
await navigator.clipboard.writeText(row.value)
|
|
342
|
+
copyBtn.textContent = '✓'
|
|
343
|
+
setTimeout(() => {
|
|
344
|
+
copyBtn.textContent = '📋'
|
|
345
|
+
}, 1200)
|
|
346
|
+
} catch (_e) {
|
|
347
|
+
copyBtn.textContent = '!'
|
|
348
|
+
setTimeout(() => {
|
|
349
|
+
copyBtn.textContent = '📋'
|
|
350
|
+
}, 1200)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
valueWrap.appendChild(copyBtn)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
line.appendChild(label)
|
|
358
|
+
line.appendChild(valueWrap)
|
|
359
|
+
details.appendChild(line)
|
|
360
|
+
|
|
361
|
+
// If this is the Battery row, add a sparkline chart below
|
|
362
|
+
if (row.label === 'Battery' && Array.isArray(batteryHistory) && batteryHistory.length > 1) {
|
|
363
|
+
const chart = document.createElement('div')
|
|
364
|
+
chart.style.margin = '2px 0 8px 0'
|
|
365
|
+
chart.style.width = '100%'
|
|
366
|
+
chart.style.height = '28px'
|
|
367
|
+
chart.style.display = 'flex'
|
|
368
|
+
// SVG sparkline
|
|
369
|
+
const w = 120, h = 24, pad = 2
|
|
370
|
+
const min = Math.min(...batteryHistory.map(b => b.value), 100)
|
|
371
|
+
const max = Math.max(...batteryHistory.map(b => b.value), 0)
|
|
372
|
+
const range = max - min || 1
|
|
373
|
+
const points = batteryHistory.map((b, i) => {
|
|
374
|
+
const x = pad + (i * (w - 2 * pad)) / (batteryHistory.length - 1)
|
|
375
|
+
const y = pad + (h - 2 * pad) * (1 - (b.value - min) / range)
|
|
376
|
+
return `${x},${y}`
|
|
377
|
+
}).join(' ')
|
|
378
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
379
|
+
svg.setAttribute('width', String(w))
|
|
380
|
+
svg.setAttribute('height', String(h))
|
|
381
|
+
svg.setAttribute('viewBox', `0 0 ${w} ${h}`)
|
|
382
|
+
svg.style.display = 'block'
|
|
383
|
+
svg.style.background = '#f3f4f6'
|
|
384
|
+
svg.style.borderRadius = '3px'
|
|
385
|
+
svg.style.marginTop = '2px'
|
|
386
|
+
svg.style.boxShadow = '0 1px 2px #0001'
|
|
387
|
+
// Polyline for trend
|
|
388
|
+
const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline')
|
|
389
|
+
polyline.setAttribute('points', points)
|
|
390
|
+
polyline.setAttribute('fill', 'none')
|
|
391
|
+
polyline.setAttribute('stroke', '#2563eb')
|
|
392
|
+
polyline.setAttribute('stroke-width', '2')
|
|
393
|
+
svg.appendChild(polyline)
|
|
394
|
+
// Dots for each point
|
|
395
|
+
batteryHistory.forEach((b, i) => {
|
|
396
|
+
const x = pad + (i * (w - 2 * pad)) / (batteryHistory.length - 1)
|
|
397
|
+
const y = pad + (h - 2 * pad) * (1 - (b.value - min) / range)
|
|
398
|
+
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
|
|
399
|
+
circle.setAttribute('cx', String(x))
|
|
400
|
+
circle.setAttribute('cy', String(y))
|
|
401
|
+
circle.setAttribute('r', '2.5')
|
|
402
|
+
circle.setAttribute('fill', '#2563eb')
|
|
403
|
+
svg.appendChild(circle)
|
|
404
|
+
})
|
|
405
|
+
// Min/max labels
|
|
406
|
+
const minLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text')
|
|
407
|
+
minLabel.setAttribute('x', '2')
|
|
408
|
+
minLabel.setAttribute('y', String(h - 2))
|
|
409
|
+
minLabel.setAttribute('font-size', '9')
|
|
410
|
+
minLabel.setAttribute('fill', '#888')
|
|
411
|
+
minLabel.textContent = `${min}%`
|
|
412
|
+
svg.appendChild(minLabel)
|
|
413
|
+
const maxLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text')
|
|
414
|
+
maxLabel.setAttribute('x', String(w - 18))
|
|
415
|
+
maxLabel.setAttribute('y', '10')
|
|
416
|
+
maxLabel.setAttribute('font-size', '9')
|
|
417
|
+
maxLabel.setAttribute('fill', '#888')
|
|
418
|
+
maxLabel.textContent = `${max}%`
|
|
419
|
+
svg.appendChild(maxLabel)
|
|
420
|
+
chart.appendChild(svg)
|
|
421
|
+
details.appendChild(chart)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// --- Expose advanced/extra features dynamically ---
|
|
426
|
+
const featureKeys = [
|
|
427
|
+
'airQuality', 'pm25', 'pm10', 'voc', 'co2', 'humidity', 'temperature',
|
|
428
|
+
'preset', 'mode', 'presetMode', 'direction', 'calibration', 'multiCommand',
|
|
429
|
+
'extendedInfo', 'segmentedControl', 'features', 'capabilities', 'state',
|
|
430
|
+
]
|
|
431
|
+
const shown = new Set(rows.map(r => r.label.toLowerCase().replace(/\s/g, '')))
|
|
432
|
+
for (const key of featureKeys) {
|
|
433
|
+
if (device && device[key] !== undefined && !shown.has(key.toLowerCase())) {
|
|
434
|
+
const line = document.createElement('div')
|
|
435
|
+
line.style.display = 'flex'
|
|
436
|
+
line.style.alignItems = 'center'
|
|
437
|
+
line.style.justifyContent = 'space-between'
|
|
438
|
+
line.style.gap = '8px'
|
|
439
|
+
line.style.padding = '2px 0'
|
|
440
|
+
|
|
441
|
+
const label = document.createElement('span')
|
|
442
|
+
label.style.fontWeight = '600'
|
|
443
|
+
label.style.minWidth = '110px'
|
|
444
|
+
label.textContent = key.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase()) + ':'
|
|
445
|
+
|
|
446
|
+
const value = document.createElement('span')
|
|
447
|
+
value.style.fontFamily = 'monospace'
|
|
448
|
+
value.style.fontSize = '11px'
|
|
449
|
+
value.style.opacity = '0.9'
|
|
450
|
+
value.style.whiteSpace = 'normal'
|
|
451
|
+
value.style.overflowWrap = 'anywhere'
|
|
452
|
+
value.style.wordBreak = 'break-word'
|
|
453
|
+
value.style.textAlign = 'right'
|
|
454
|
+
value.textContent = typeof device[key] === 'object' ? JSON.stringify(device[key]) : String(device[key])
|
|
455
|
+
|
|
456
|
+
line.appendChild(label)
|
|
457
|
+
line.appendChild(value)
|
|
458
|
+
details.appendChild(line)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return details
|
|
463
|
+
}
|
|
464
|
+
|
|
179
465
|
export async function renderDiscoveredDevices(
|
|
180
466
|
devices: any[],
|
|
181
|
-
options: {
|
|
467
|
+
options: {
|
|
468
|
+
configuredIds?: Set<string>
|
|
469
|
+
selectedIds?: Set<string>
|
|
470
|
+
onToggleSelect?: (device: any, selected: boolean) => void
|
|
471
|
+
} = {},
|
|
182
472
|
): Promise<HTMLElement> {
|
|
183
473
|
const ul = document.createElement('ul')
|
|
184
474
|
ul.className = 'device-grid'
|
|
@@ -191,6 +481,8 @@ export async function renderDiscoveredDevices(
|
|
|
191
481
|
const { addDeviceToConfig } = await import('./discovery.js')
|
|
192
482
|
const { loadConfiguredDevices } = await import('./devices.js')
|
|
193
483
|
const configuredIds = options.configuredIds ?? new Set<string>()
|
|
484
|
+
const selectedIds = options.selectedIds ?? new Set<string>()
|
|
485
|
+
const onToggleSelect = options.onToggleSelect
|
|
194
486
|
|
|
195
487
|
for (const d of devices) {
|
|
196
488
|
const deviceId = normalizeId(d.id)
|
|
@@ -224,8 +516,76 @@ export async function renderDiscoveredDevices(
|
|
|
224
516
|
name.style.fontSize = '13px'
|
|
225
517
|
name.textContent = d.name || d.id
|
|
226
518
|
|
|
519
|
+
const selectCheckbox = document.createElement('input')
|
|
520
|
+
selectCheckbox.type = 'checkbox'
|
|
521
|
+
selectCheckbox.style.width = 'auto'
|
|
522
|
+
selectCheckbox.style.margin = '0 2px 0 0'
|
|
523
|
+
selectCheckbox.checked = selectedIds.has(deviceId)
|
|
524
|
+
if (alreadyAdded) {
|
|
525
|
+
selectCheckbox.disabled = true
|
|
526
|
+
selectCheckbox.title = 'Already configured'
|
|
527
|
+
}
|
|
528
|
+
selectCheckbox.onchange = () => {
|
|
529
|
+
onToggleSelect?.(d, selectCheckbox.checked)
|
|
530
|
+
// Notify listeners (e.g., batch buttons) of selection change
|
|
531
|
+
window.dispatchEvent(new CustomEvent('discovery-selection-changed'))
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
nameContainer.appendChild(selectCheckbox)
|
|
535
|
+
|
|
536
|
+
|
|
227
537
|
nameContainer.appendChild(name)
|
|
228
538
|
|
|
539
|
+
// Show firmware update available indicator if present
|
|
540
|
+
if (d.firmwareUpdateAvailable) {
|
|
541
|
+
const fwBadge = document.createElement('span');
|
|
542
|
+
fwBadge.textContent = 'Update Available';
|
|
543
|
+
fwBadge.style.cssText = 'background: #fb923c; color: #111; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;';
|
|
544
|
+
fwBadge.title = 'A firmware update is available for this device.';
|
|
545
|
+
nameContainer.appendChild(fwBadge);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Show offline/unreachable indicator if device is offline
|
|
549
|
+
let offline = false;
|
|
550
|
+
let lastSeen = d.lastSeen || d.lastseen || d.updatedAt;
|
|
551
|
+
if (typeof d.offline === 'boolean') {
|
|
552
|
+
offline = d.offline;
|
|
553
|
+
} else if (lastSeen) {
|
|
554
|
+
try {
|
|
555
|
+
const last = new Date(lastSeen).getTime();
|
|
556
|
+
if (!isNaN(last) && Date.now() - last > 1000 * 60 * 60) { // 1 hour
|
|
557
|
+
offline = true;
|
|
558
|
+
}
|
|
559
|
+
} catch {}
|
|
560
|
+
}
|
|
561
|
+
if (offline) {
|
|
562
|
+
const offlineBadge = document.createElement('span');
|
|
563
|
+
offlineBadge.textContent = 'Offline';
|
|
564
|
+
offlineBadge.style.cssText = 'background: #dc2626; color: white; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;';
|
|
565
|
+
offlineBadge.title = 'Device is offline or unreachable.';
|
|
566
|
+
nameContainer.appendChild(offlineBadge);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const expandedDetails = document.createElement('div')
|
|
570
|
+
expandedDetails.style.display = 'none'
|
|
571
|
+
expandedDetails.appendChild(renderDeviceDetailsPanel(d))
|
|
572
|
+
|
|
573
|
+
const expandBtn = document.createElement('button')
|
|
574
|
+
expandBtn.textContent = '▾'
|
|
575
|
+
expandBtn.title = 'Show details'
|
|
576
|
+
expandBtn.style.padding = '2px 6px'
|
|
577
|
+
expandBtn.style.fontSize = '11px'
|
|
578
|
+
expandBtn.style.marginLeft = '4px'
|
|
579
|
+
expandBtn.style.background = '#e5e7eb'
|
|
580
|
+
expandBtn.style.color = '#111827'
|
|
581
|
+
expandBtn.style.transition = 'transform 0.2s ease'
|
|
582
|
+
expandBtn.onclick = () => {
|
|
583
|
+
const isHidden = expandedDetails.style.display === 'none'
|
|
584
|
+
expandedDetails.style.display = isHidden ? 'block' : 'none'
|
|
585
|
+
expandBtn.style.transform = isHidden ? 'rotate(180deg)' : 'rotate(0deg)'
|
|
586
|
+
}
|
|
587
|
+
nameContainer.appendChild(expandBtn)
|
|
588
|
+
|
|
229
589
|
const duplicateBadge = document.createElement('span')
|
|
230
590
|
duplicateBadge.textContent = alreadyAdded ? '✓ Already Added' : '➕ New Device'
|
|
231
591
|
duplicateBadge.style.cssText = alreadyAdded
|
|
@@ -250,6 +610,18 @@ export async function renderDiscoveredDevices(
|
|
|
250
610
|
nameContainer.appendChild(renderSignalQualityBadge(d.rssi))
|
|
251
611
|
}
|
|
252
612
|
|
|
613
|
+
// Add battery warning indicator if battery < 20%
|
|
614
|
+
if (typeof d.battery === 'number' && d.battery < 20) {
|
|
615
|
+
const batteryWarn = document.createElement('span');
|
|
616
|
+
batteryWarn.textContent = `⚠️ ${d.battery}%`;
|
|
617
|
+
batteryWarn.style.cssText =
|
|
618
|
+
d.battery < 10
|
|
619
|
+
? 'background: #dc2626; color: white; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;'
|
|
620
|
+
: 'background: #fbbf24; color: #111; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;';
|
|
621
|
+
batteryWarn.title = d.battery < 10 ? 'Battery critically low' : 'Battery low';
|
|
622
|
+
nameContainer.appendChild(batteryWarn);
|
|
623
|
+
}
|
|
624
|
+
|
|
253
625
|
const details = document.createElement('div')
|
|
254
626
|
details.style.fontSize = '10px'
|
|
255
627
|
details.style.opacity = '0.7'
|
|
@@ -266,6 +638,7 @@ export async function renderDiscoveredDevices(
|
|
|
266
638
|
|
|
267
639
|
info.appendChild(nameContainer)
|
|
268
640
|
info.appendChild(details)
|
|
641
|
+
info.appendChild(expandedDetails)
|
|
269
642
|
|
|
270
643
|
const addBtn = document.createElement('button')
|
|
271
644
|
addBtn.textContent = alreadyAdded ? 'Already Added' : 'Add to Config'
|
|
@@ -312,6 +685,7 @@ export async function renderDiscoveredDevices(
|
|
|
312
685
|
actions.style.gap = '5px'
|
|
313
686
|
actions.appendChild(viewBtn)
|
|
314
687
|
actions.appendChild(addBtn)
|
|
688
|
+
actions.appendChild(createConnectionTestControls(d))
|
|
315
689
|
li.appendChild(actions)
|
|
316
690
|
ul.appendChild(li)
|
|
317
691
|
continue
|
|
@@ -327,6 +701,7 @@ export async function renderDiscoveredDevices(
|
|
|
327
701
|
actions.style.marginTop = '2px'
|
|
328
702
|
actions.style.gap = '5px'
|
|
329
703
|
actions.appendChild(addBtn)
|
|
704
|
+
actions.appendChild(createConnectionTestControls(d))
|
|
330
705
|
|
|
331
706
|
li.appendChild(info)
|
|
332
707
|
li.appendChild(actions)
|
|
@@ -533,6 +908,25 @@ export function renderDeviceList(list: any[]): void {
|
|
|
533
908
|
name.style.fontSize = '13px'
|
|
534
909
|
name.textContent = d.name || d.id
|
|
535
910
|
|
|
911
|
+
const expandedDetails = document.createElement('div')
|
|
912
|
+
expandedDetails.style.display = 'none'
|
|
913
|
+
expandedDetails.appendChild(renderDeviceDetailsPanel(d))
|
|
914
|
+
|
|
915
|
+
const expandBtn = document.createElement('button')
|
|
916
|
+
expandBtn.textContent = '▾'
|
|
917
|
+
expandBtn.title = 'Show details'
|
|
918
|
+
expandBtn.style.padding = '2px 6px'
|
|
919
|
+
expandBtn.style.fontSize = '11px'
|
|
920
|
+
expandBtn.style.marginLeft = '4px'
|
|
921
|
+
expandBtn.style.background = '#e5e7eb'
|
|
922
|
+
expandBtn.style.color = '#111827'
|
|
923
|
+
expandBtn.style.transition = 'transform 0.2s ease'
|
|
924
|
+
expandBtn.onclick = () => {
|
|
925
|
+
const isHidden = expandedDetails.style.display === 'none'
|
|
926
|
+
expandedDetails.style.display = isHidden ? 'block' : 'none'
|
|
927
|
+
expandBtn.style.transform = isHidden ? 'rotate(180deg)' : 'rotate(0deg)'
|
|
928
|
+
}
|
|
929
|
+
|
|
536
930
|
const code = document.createElement('code')
|
|
537
931
|
code.textContent = d.id
|
|
538
932
|
code.style.fontSize = '10px'
|
|
@@ -543,7 +937,14 @@ export function renderDeviceList(list: any[]): void {
|
|
|
543
937
|
code.style.wordBreak = 'break-word'
|
|
544
938
|
code.style.maxWidth = '100%'
|
|
545
939
|
|
|
546
|
-
|
|
940
|
+
const headerRow = document.createElement('div')
|
|
941
|
+
headerRow.style.display = 'inline-flex'
|
|
942
|
+
headerRow.style.alignItems = 'center'
|
|
943
|
+
headerRow.style.gap = '4px'
|
|
944
|
+
headerRow.appendChild(name)
|
|
945
|
+
headerRow.appendChild(expandBtn)
|
|
946
|
+
|
|
947
|
+
nameContainer.appendChild(headerRow)
|
|
547
948
|
nameContainer.appendChild(code)
|
|
548
949
|
|
|
549
950
|
// Add signal strength visualization if RSSI is available
|
|
@@ -564,6 +965,7 @@ export function renderDeviceList(list: any[]): void {
|
|
|
564
965
|
|
|
565
966
|
info.appendChild(nameContainer)
|
|
566
967
|
info.appendChild(meta)
|
|
968
|
+
info.appendChild(expandedDetails)
|
|
567
969
|
|
|
568
970
|
const buttons = document.createElement('div')
|
|
569
971
|
buttons.className = 'device-actions'
|
|
@@ -614,6 +1016,7 @@ export function renderDeviceList(list: any[]): void {
|
|
|
614
1016
|
|
|
615
1017
|
buttons.appendChild(editBtn)
|
|
616
1018
|
buttons.appendChild(copyBtn)
|
|
1019
|
+
buttons.appendChild(createConnectionTestControls(d))
|
|
617
1020
|
buttons.appendChild(deleteBtn)
|
|
618
1021
|
|
|
619
1022
|
li.appendChild(info)
|
package/dist/platform.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SwitchBotPluginConfig } from './settings.js';
|
|
2
2
|
import type { API, Logger, PlatformConfig } from 'homebridge';
|
|
3
|
+
export declare const DEVICE_MATTER_CLUSTERS: Record<string, any>;
|
|
3
4
|
export declare class SwitchBotHAPPlatform {
|
|
4
5
|
api: API | undefined;
|
|
5
6
|
log: Logger;
|
package/dist/platform.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAC1D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAC1D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAkE7D,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CA2ItD,CAAA;AA0yBD,qBAAa,oBAAoB;IAC/B,GAAG,EAAE,GAAG,GAAG,SAAS,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,qBAAqB,CAAA;IAC7B,OAAO,EAAE,GAAG,EAAE,CAAK;IAEnB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAE7B,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,oBAAoB,CAA8B;gBAE9C,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,GAAG;IA2C1D,OAAO,CAAC,aAAa;YAUP,qBAAqB;IAU7B,WAAW;IAsKX,kBAAkB,CAAC,SAAS,EAAE,GAAG;IAYvC,wBAAwB,CAAC,CAAC,SAAS,EAAE,GAAG;CASzC;AAGD,qBAAa,uBAAuB;IAClC,GAAG,EAAE,GAAG,GAAG,SAAS,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,qBAAqB,CAAA;IAC7B,OAAO,EAAE,GAAG,EAAE,CAAK;IACnB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAE7B,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,oBAAoB,CAA8B;gBAE9C,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,GAAG;IA8CpD,WAAW;IAwCjB,OAAO,CAAC,aAAa;YAUP,qBAAqB;IAU7B,kBAAkB,CAAC,SAAS,EAAE,GAAG;IAWvC,wBAAwB,CAAC,SAAS,EAAE,GAAG;IAWjC,yBAAyB;CAqHhC;AAED,eAAe,oBAAoB,CAAA"}
|
package/dist/platform.js
CHANGED
|
@@ -55,7 +55,7 @@ const DEVICE_MATTER_SUPPORTED = {
|
|
|
55
55
|
// Default Matter cluster configurations by device type
|
|
56
56
|
// Maps device types to their Matter cluster states (used when device doesn't provide clusters)
|
|
57
57
|
// Note: wosweeper/curtain/plug variants are normalized before cluster lookup (see loadDevices)
|
|
58
|
-
const DEVICE_MATTER_CLUSTERS = {
|
|
58
|
+
export const DEVICE_MATTER_CLUSTERS = {
|
|
59
59
|
// Core devices - aligned with HAP service implementations
|
|
60
60
|
bot: { onOff: { onOff: false } }, // Switch → OnOff
|
|
61
61
|
vacuum: {
|
|
@@ -87,7 +87,7 @@ const DEVICE_MATTER_CLUSTERS = {
|
|
|
87
87
|
}, // Switch in HAP, RobotVacuumCleaner in Matter
|
|
88
88
|
curtain: {
|
|
89
89
|
windowCovering: {
|
|
90
|
-
// Use
|
|
90
|
+
// Use Rollershade (0) for lift-only/non-tilt covering to match test expectation.
|
|
91
91
|
// Drapery (4) can fail validation in some stacks when TL (tilt) is not present.
|
|
92
92
|
type: 0, // Rollershade
|
|
93
93
|
},
|
|
@@ -122,7 +122,9 @@ const DEVICE_MATTER_CLUSTERS = {
|
|
|
122
122
|
minLevel: 0,
|
|
123
123
|
maxLevel: 254,
|
|
124
124
|
},
|
|
125
|
-
colorControl: {
|
|
125
|
+
colorControl: {
|
|
126
|
+
colorMode: 0,
|
|
127
|
+
},
|
|
126
128
|
}, // Lightbulb with color → OnOff + LevelControl + ColorControl
|
|
127
129
|
lock: {
|
|
128
130
|
doorLock: {
|
|
@@ -1078,6 +1080,8 @@ export class SwitchBotHAPPlatform {
|
|
|
1078
1080
|
id: raw.deviceId ?? raw.id,
|
|
1079
1081
|
name: raw.configDeviceName ?? raw.name,
|
|
1080
1082
|
type: raw.configDeviceType ?? raw.type ?? raw.deviceType ?? 'unknown',
|
|
1083
|
+
encryptionKey: raw.encryptionKey,
|
|
1084
|
+
keyId: raw.keyId,
|
|
1081
1085
|
_raw: raw,
|
|
1082
1086
|
};
|
|
1083
1087
|
const type = normalizeTypeForMatter(d.type);
|