@switchbot/homebridge-switchbot 5.0.0-beta.99 → 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 -471
- 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 -554
- 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 -945
- package/dist/platform.js.map +0 -1
- package/src/platform.ts +0 -963
- 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,1084 @@
|
|
|
1
|
+
// Move regexes to module scope to avoid re-compilation on every call
|
|
2
|
+
// import type { DEVICE_TYPES } from './constants.js' // Removed unused import
|
|
3
|
+
|
|
4
|
+
const SPACES_REGEX = /\s/g
|
|
5
|
+
const CAMELCASE_REGEX = /([A-Z])/g
|
|
6
|
+
const FIRST_CHAR_REGEX = /^./
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get RSSI signal quality level and color based on dBm value
|
|
10
|
+
* @param rssi Signal strength in dBm (typically -30 to -90)
|
|
11
|
+
* @returns Object with quality level, color, and description
|
|
12
|
+
*/
|
|
13
|
+
export function getRssiSignalQuality(rssi: number | undefined): {
|
|
14
|
+
level: 'excellent' | 'good' | 'fair' | 'poor' | 'unknown'
|
|
15
|
+
color: string
|
|
16
|
+
bgColor: string
|
|
17
|
+
description: string
|
|
18
|
+
bars: number
|
|
19
|
+
} {
|
|
20
|
+
if (!rssi || rssi === 0) {
|
|
21
|
+
return {
|
|
22
|
+
level: 'unknown',
|
|
23
|
+
color: '#999',
|
|
24
|
+
bgColor: '#f5f5f5',
|
|
25
|
+
description: 'Signal strength unknown',
|
|
26
|
+
bars: 0,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const dbm = Math.floor(rssi)
|
|
31
|
+
|
|
32
|
+
if (dbm > -60) {
|
|
33
|
+
return {
|
|
34
|
+
level: 'excellent',
|
|
35
|
+
color: '#34a853',
|
|
36
|
+
bgColor: '#e8f5e9',
|
|
37
|
+
description: `Excellent (${dbm} dBm)`,
|
|
38
|
+
bars: 4,
|
|
39
|
+
}
|
|
40
|
+
} else if (dbm > -75) {
|
|
41
|
+
return {
|
|
42
|
+
level: 'good',
|
|
43
|
+
color: '#fbbc04',
|
|
44
|
+
bgColor: '#fffde7',
|
|
45
|
+
description: `Good (${dbm} dBm)`,
|
|
46
|
+
bars: 3,
|
|
47
|
+
}
|
|
48
|
+
} else if (dbm > -85) {
|
|
49
|
+
return {
|
|
50
|
+
level: 'fair',
|
|
51
|
+
color: '#ff9800',
|
|
52
|
+
bgColor: '#fff3e0',
|
|
53
|
+
description: `Fair (${dbm} dBm)`,
|
|
54
|
+
bars: 2,
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
return {
|
|
58
|
+
level: 'poor',
|
|
59
|
+
color: '#ea4335',
|
|
60
|
+
bgColor: '#ffebee',
|
|
61
|
+
description: `Poor (${dbm} dBm) - unreliable`,
|
|
62
|
+
bars: 1,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create visual signal strength indicator bars
|
|
69
|
+
* @param rssi Signal strength in dBm
|
|
70
|
+
* @returns HTML element showing filled bars
|
|
71
|
+
*/
|
|
72
|
+
export function renderSignalBars(rssi: number | undefined): HTMLElement {
|
|
73
|
+
const quality = getRssiSignalQuality(rssi)
|
|
74
|
+
|
|
75
|
+
const container = document.createElement('span')
|
|
76
|
+
container.style.display = 'inline-flex'
|
|
77
|
+
container.style.gap = '2px'
|
|
78
|
+
container.style.alignItems = 'center'
|
|
79
|
+
container.style.marginLeft = '8px'
|
|
80
|
+
container.style.fontSize = '12px'
|
|
81
|
+
|
|
82
|
+
// Create 4 bars
|
|
83
|
+
for (let i = 1; i <= 4; i++) {
|
|
84
|
+
const bar = document.createElement('span')
|
|
85
|
+
bar.style.height = `${i * 3}px`
|
|
86
|
+
bar.style.width = '3px'
|
|
87
|
+
bar.style.borderRadius = '1px'
|
|
88
|
+
bar.style.border = `1px solid ${quality.color}`
|
|
89
|
+
|
|
90
|
+
if (i <= quality.bars) {
|
|
91
|
+
bar.style.backgroundColor = quality.color
|
|
92
|
+
} else {
|
|
93
|
+
bar.style.backgroundColor = 'transparent'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
container.appendChild(bar)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Add tooltip
|
|
100
|
+
container.title = quality.description
|
|
101
|
+
|
|
102
|
+
return container
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create signal quality badge with color
|
|
107
|
+
* @param rssi Signal strength in dBm
|
|
108
|
+
* @returns HTML element showing quality level
|
|
109
|
+
*/
|
|
110
|
+
export function renderSignalQualityBadge(rssi: number | undefined): HTMLElement {
|
|
111
|
+
const quality = getRssiSignalQuality(rssi)
|
|
112
|
+
|
|
113
|
+
const badge = document.createElement('span')
|
|
114
|
+
badge.textContent = quality.level.charAt(0).toUpperCase() + quality.level.slice(1)
|
|
115
|
+
badge.style.cssText = `
|
|
116
|
+
background: ${quality.color};
|
|
117
|
+
color: white;
|
|
118
|
+
padding: 2px 6px;
|
|
119
|
+
border-radius: 3px;
|
|
120
|
+
font-size: 10px;
|
|
121
|
+
font-weight: 600;
|
|
122
|
+
margin-left: 8px;
|
|
123
|
+
`
|
|
124
|
+
badge.title = quality.description
|
|
125
|
+
|
|
126
|
+
return badge
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function renderBadge(text: string, style: string): HTMLElement {
|
|
130
|
+
const badge = document.createElement('span')
|
|
131
|
+
badge.textContent = text
|
|
132
|
+
badge.style.cssText = style
|
|
133
|
+
return badge
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function renderConnectionBadge(connectionType: string): HTMLElement | null {
|
|
137
|
+
if (!connectionType) {
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const badge = renderBadge(connectionType, '')
|
|
142
|
+
|
|
143
|
+
if (connectionType === 'BLE') {
|
|
144
|
+
badge.style.cssText
|
|
145
|
+
= 'background: #4285f4; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 8px;'
|
|
146
|
+
} else if (connectionType === 'Both') {
|
|
147
|
+
badge.style.cssText
|
|
148
|
+
= 'background: #34a853; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 8px;'
|
|
149
|
+
} else {
|
|
150
|
+
badge.style.cssText
|
|
151
|
+
= 'background: #9e9e9e; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 8px;'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return badge
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function renderIRBadge(): HTMLElement {
|
|
158
|
+
return renderBadge(
|
|
159
|
+
'IR',
|
|
160
|
+
'background: #ff6b35; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 8px;',
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizeId(value: any): string {
|
|
165
|
+
return String(value ?? '').trim().toLowerCase()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function scrollToConfiguredDevice(deviceId: string): void {
|
|
169
|
+
const normalizedId = normalizeId(deviceId)
|
|
170
|
+
const target = document.querySelector(`[data-device-id="${normalizedId}"]`) as HTMLElement | null
|
|
171
|
+
if (!target) {
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
176
|
+
const originalOutline = target.style.outline
|
|
177
|
+
const originalBackground = target.style.background
|
|
178
|
+
target.style.outline = '2px solid var(--switchbot-red, #ef4444)'
|
|
179
|
+
target.style.background = 'rgba(239, 68, 68, 0.08)'
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
target.style.outline = originalOutline
|
|
182
|
+
target.style.background = originalBackground
|
|
183
|
+
}, 1800)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function createConnectionTestControls(device: any): HTMLElement {
|
|
187
|
+
const controls = document.createElement('div')
|
|
188
|
+
controls.style.display = 'inline-flex'
|
|
189
|
+
controls.style.alignItems = 'center'
|
|
190
|
+
controls.style.gap = '6px'
|
|
191
|
+
|
|
192
|
+
const button = document.createElement('button')
|
|
193
|
+
button.textContent = 'Test Connection'
|
|
194
|
+
button.className = 'secondary'
|
|
195
|
+
button.style.padding = '4px 9px'
|
|
196
|
+
button.style.fontSize = '11px'
|
|
197
|
+
|
|
198
|
+
const status = document.createElement('span')
|
|
199
|
+
status.style.fontSize = '10px'
|
|
200
|
+
status.style.opacity = '0.85'
|
|
201
|
+
status.style.whiteSpace = 'normal'
|
|
202
|
+
status.style.overflowWrap = 'anywhere'
|
|
203
|
+
|
|
204
|
+
button.onclick = async () => {
|
|
205
|
+
const startedAt = Date.now()
|
|
206
|
+
button.disabled = true
|
|
207
|
+
button.textContent = 'Testing...'
|
|
208
|
+
status.textContent = 'Checking...'
|
|
209
|
+
status.style.color = '#6b7280'
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const { testDeviceConnection } = await import('./api.js')
|
|
213
|
+
const result = await testDeviceConnection({
|
|
214
|
+
deviceId: String(device?.id || device?.deviceId || ''),
|
|
215
|
+
connectionType: device?.connectionType,
|
|
216
|
+
address: device?.address,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const measuredLatency = Number(result?.latencyMs) > 0
|
|
220
|
+
? Number(result.latencyMs)
|
|
221
|
+
: Date.now() - startedAt
|
|
222
|
+
|
|
223
|
+
if (result?.success) {
|
|
224
|
+
const method = result?.method || 'Auto'
|
|
225
|
+
status.textContent = `✓ ${method} · ${measuredLatency}ms`
|
|
226
|
+
status.style.color = '#16a34a'
|
|
227
|
+
} else {
|
|
228
|
+
const detail = result?.message ? ` · ${result.message}` : ''
|
|
229
|
+
status.textContent = `✗ Failed · ${measuredLatency}ms${detail}`
|
|
230
|
+
status.style.color = '#dc2626'
|
|
231
|
+
}
|
|
232
|
+
} catch (e) {
|
|
233
|
+
status.textContent = `✗ Failed · ${Date.now() - startedAt}ms`
|
|
234
|
+
status.style.color = '#dc2626'
|
|
235
|
+
} finally {
|
|
236
|
+
button.disabled = false
|
|
237
|
+
button.textContent = 'Test Connection'
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
controls.appendChild(button)
|
|
242
|
+
controls.appendChild(status)
|
|
243
|
+
return controls
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function formatLastSeen(value: any): string {
|
|
247
|
+
if (!value) {
|
|
248
|
+
return 'N/A'
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const date = new Date(value)
|
|
252
|
+
return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString()
|
|
253
|
+
} catch (_e) {
|
|
254
|
+
return String(value)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function renderDeviceDetailsPanel(device: any): HTMLElement {
|
|
259
|
+
const details = document.createElement('div')
|
|
260
|
+
details.className = 'device-details-panel'
|
|
261
|
+
details.style.borderTop = '1px solid #ddd'
|
|
262
|
+
details.style.padding = '8px'
|
|
263
|
+
details.style.borderRadius = '4px'
|
|
264
|
+
details.style.fontSize = '12px'
|
|
265
|
+
details.style.marginTop = '4px'
|
|
266
|
+
|
|
267
|
+
// --- Battery history trending ---
|
|
268
|
+
// Persist battery readings in localStorage per device
|
|
269
|
+
const batteryHistoryKey = `batteryHistory:${device?.id || device?.deviceId}`
|
|
270
|
+
let batteryHistory: Array<{ value: number, ts: number }> = []
|
|
271
|
+
try {
|
|
272
|
+
const raw = localStorage.getItem(batteryHistoryKey)
|
|
273
|
+
if (raw) {
|
|
274
|
+
batteryHistory = JSON.parse(raw)
|
|
275
|
+
}
|
|
276
|
+
} catch (e) {
|
|
277
|
+
// Optionally log or handle error
|
|
278
|
+
}
|
|
279
|
+
const now = Date.now()
|
|
280
|
+
if (typeof device?.battery === 'number') {
|
|
281
|
+
// Only add if different from last or >1h since last
|
|
282
|
+
const last = batteryHistory.at(-1)
|
|
283
|
+
if (!last || last.value !== device.battery || now - last.ts > 60 * 60 * 1000) {
|
|
284
|
+
batteryHistory.push({ value: device.battery, ts: now })
|
|
285
|
+
// Keep only last 30 entries (about a month if daily)
|
|
286
|
+
if (batteryHistory.length > 30) {
|
|
287
|
+
batteryHistory = batteryHistory.slice(-30)
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
localStorage.setItem(batteryHistoryKey, JSON.stringify(batteryHistory))
|
|
291
|
+
} catch (e) {
|
|
292
|
+
// Optionally log or handle error
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const rows: Array<{ label: string, value: string, copyable?: boolean }> = [
|
|
298
|
+
{ label: 'Name', value: String(device?.name || device?.configDeviceName || 'N/A') },
|
|
299
|
+
{ label: 'Device ID', value: String(device?.id || device?.deviceId || 'N/A'), copyable: !!(device?.id || device?.deviceId) },
|
|
300
|
+
{ label: 'MAC Address', value: String(device?.address || 'N/A'), copyable: !!device?.address },
|
|
301
|
+
{ label: 'Device Type', value: String(device?.type || device?.configDeviceType || 'N/A') },
|
|
302
|
+
{ label: 'Model', value: String(device?.model || 'N/A') },
|
|
303
|
+
{ label: 'Hub ID', value: String(device?.hubDeviceId || 'N/A') },
|
|
304
|
+
{ label: 'Battery', value: device?.battery !== undefined && device?.battery !== null ? `${device.battery}%` : 'N/A' },
|
|
305
|
+
{ label: 'Firmware', value: String(device?.version || device?.firmware || 'N/A') },
|
|
306
|
+
{ label: 'Cloud Service', value: device?.enabled === false ? 'Disabled' : 'Enabled' },
|
|
307
|
+
{ label: 'Last Seen', value: formatLastSeen(device?.lastSeen || device?.lastseen || device?.updatedAt) },
|
|
308
|
+
]
|
|
309
|
+
|
|
310
|
+
for (const row of rows) {
|
|
311
|
+
const line = document.createElement('div')
|
|
312
|
+
line.style.display = 'flex'
|
|
313
|
+
line.style.alignItems = 'center'
|
|
314
|
+
line.style.justifyContent = 'space-between'
|
|
315
|
+
line.style.gap = '8px'
|
|
316
|
+
line.style.padding = '2px 0'
|
|
317
|
+
|
|
318
|
+
const label = document.createElement('span')
|
|
319
|
+
label.style.fontWeight = '600'
|
|
320
|
+
label.style.minWidth = '110px'
|
|
321
|
+
label.textContent = `${row.label}:`
|
|
322
|
+
|
|
323
|
+
const valueWrap = document.createElement('span')
|
|
324
|
+
valueWrap.style.display = 'inline-flex'
|
|
325
|
+
valueWrap.style.alignItems = 'center'
|
|
326
|
+
valueWrap.style.gap = '6px'
|
|
327
|
+
valueWrap.style.flex = '1'
|
|
328
|
+
valueWrap.style.justifyContent = 'flex-end'
|
|
329
|
+
valueWrap.style.minWidth = '0'
|
|
330
|
+
|
|
331
|
+
const value = document.createElement('span')
|
|
332
|
+
value.style.fontFamily = 'monospace'
|
|
333
|
+
value.style.fontSize = '11px'
|
|
334
|
+
value.style.opacity = '0.9'
|
|
335
|
+
value.style.whiteSpace = 'normal'
|
|
336
|
+
value.style.overflowWrap = 'anywhere'
|
|
337
|
+
value.style.wordBreak = 'break-word'
|
|
338
|
+
value.style.textAlign = 'right'
|
|
339
|
+
value.textContent = row.value
|
|
340
|
+
|
|
341
|
+
valueWrap.appendChild(value)
|
|
342
|
+
|
|
343
|
+
if (row.copyable && row.value && row.value !== 'N/A') {
|
|
344
|
+
const copyBtn = document.createElement('button')
|
|
345
|
+
copyBtn.textContent = '📋'
|
|
346
|
+
copyBtn.title = `Copy ${row.label}`
|
|
347
|
+
copyBtn.style.padding = '2px 6px'
|
|
348
|
+
copyBtn.style.fontSize = '10px'
|
|
349
|
+
copyBtn.style.lineHeight = '1'
|
|
350
|
+
copyBtn.style.background = '#e5e7eb'
|
|
351
|
+
copyBtn.style.color = '#111827'
|
|
352
|
+
|
|
353
|
+
copyBtn.onclick = async () => {
|
|
354
|
+
try {
|
|
355
|
+
await navigator.clipboard.writeText(row.value)
|
|
356
|
+
copyBtn.textContent = '✓'
|
|
357
|
+
setTimeout(() => {
|
|
358
|
+
copyBtn.textContent = '📋'
|
|
359
|
+
}, 1200)
|
|
360
|
+
} catch (_e) {
|
|
361
|
+
copyBtn.textContent = '!'
|
|
362
|
+
setTimeout(() => {
|
|
363
|
+
copyBtn.textContent = '📋'
|
|
364
|
+
}, 1200)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
valueWrap.appendChild(copyBtn)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
line.appendChild(label)
|
|
372
|
+
line.appendChild(valueWrap)
|
|
373
|
+
details.appendChild(line)
|
|
374
|
+
|
|
375
|
+
// If this is the Battery row, add a sparkline chart below
|
|
376
|
+
if (row.label === 'Battery' && Array.isArray(batteryHistory) && batteryHistory.length > 1) {
|
|
377
|
+
const chart = document.createElement('div')
|
|
378
|
+
chart.style.margin = '2px 0 8px 0'
|
|
379
|
+
chart.style.width = '100%'
|
|
380
|
+
chart.style.height = '28px'
|
|
381
|
+
chart.style.display = 'flex'
|
|
382
|
+
// SVG sparkline
|
|
383
|
+
const w = 120
|
|
384
|
+
const h = 24
|
|
385
|
+
const pad = 2
|
|
386
|
+
const min = Math.min(...batteryHistory.map(b => b.value), 100)
|
|
387
|
+
const max = Math.max(...batteryHistory.map(b => b.value), 0)
|
|
388
|
+
const range = max - min || 1
|
|
389
|
+
const points = batteryHistory.map((b, i) => {
|
|
390
|
+
const x = pad + (i * (w - 2 * pad)) / (batteryHistory.length - 1)
|
|
391
|
+
const y = pad + (h - 2 * pad) * (1 - (b.value - min) / range)
|
|
392
|
+
return `${x},${y}`
|
|
393
|
+
}).join(' ')
|
|
394
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
395
|
+
svg.setAttribute('width', String(w))
|
|
396
|
+
svg.setAttribute('height', String(h))
|
|
397
|
+
svg.setAttribute('viewBox', `0 0 ${w} ${h}`)
|
|
398
|
+
svg.style.display = 'block'
|
|
399
|
+
svg.style.background = '#f3f4f6'
|
|
400
|
+
svg.style.borderRadius = '3px'
|
|
401
|
+
svg.style.marginTop = '2px'
|
|
402
|
+
svg.style.boxShadow = '0 1px 2px #0001'
|
|
403
|
+
// Polyline for trend
|
|
404
|
+
const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline')
|
|
405
|
+
polyline.setAttribute('points', points)
|
|
406
|
+
polyline.setAttribute('fill', 'none')
|
|
407
|
+
polyline.setAttribute('stroke', '#2563eb')
|
|
408
|
+
polyline.setAttribute('stroke-width', '2')
|
|
409
|
+
svg.appendChild(polyline)
|
|
410
|
+
// Dots for each point
|
|
411
|
+
batteryHistory.forEach((b, i) => {
|
|
412
|
+
const x = pad + (i * (w - 2 * pad)) / (batteryHistory.length - 1)
|
|
413
|
+
const y = pad + (h - 2 * pad) * (1 - (b.value - min) / range)
|
|
414
|
+
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
|
|
415
|
+
circle.setAttribute('cx', String(x))
|
|
416
|
+
circle.setAttribute('cy', String(y))
|
|
417
|
+
circle.setAttribute('r', '2.5')
|
|
418
|
+
circle.setAttribute('fill', '#2563eb')
|
|
419
|
+
svg.appendChild(circle)
|
|
420
|
+
})
|
|
421
|
+
// Min/max labels
|
|
422
|
+
const minLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text')
|
|
423
|
+
minLabel.setAttribute('x', '2')
|
|
424
|
+
minLabel.setAttribute('y', String(h - 2))
|
|
425
|
+
minLabel.setAttribute('font-size', '9')
|
|
426
|
+
minLabel.setAttribute('fill', '#888')
|
|
427
|
+
minLabel.textContent = `${min}%`
|
|
428
|
+
svg.appendChild(minLabel)
|
|
429
|
+
const maxLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text')
|
|
430
|
+
maxLabel.setAttribute('x', String(w - 18))
|
|
431
|
+
maxLabel.setAttribute('y', '10')
|
|
432
|
+
maxLabel.setAttribute('font-size', '9')
|
|
433
|
+
maxLabel.setAttribute('fill', '#888')
|
|
434
|
+
maxLabel.textContent = `${max}%`
|
|
435
|
+
svg.appendChild(maxLabel)
|
|
436
|
+
chart.appendChild(svg)
|
|
437
|
+
details.appendChild(chart)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// --- Expose advanced/extra features dynamically ---
|
|
442
|
+
const featureKeys = [
|
|
443
|
+
'airQuality',
|
|
444
|
+
'pm25',
|
|
445
|
+
'pm10',
|
|
446
|
+
'voc',
|
|
447
|
+
'co2',
|
|
448
|
+
'humidity',
|
|
449
|
+
'temperature',
|
|
450
|
+
'preset',
|
|
451
|
+
'mode',
|
|
452
|
+
'presetMode',
|
|
453
|
+
'direction',
|
|
454
|
+
'calibration',
|
|
455
|
+
'multiCommand',
|
|
456
|
+
'extendedInfo',
|
|
457
|
+
'segmentedControl',
|
|
458
|
+
'features',
|
|
459
|
+
'capabilities',
|
|
460
|
+
'state',
|
|
461
|
+
]
|
|
462
|
+
const shown = new Set(rows.map(r => r.label.toLowerCase().replace(SPACES_REGEX, '')))
|
|
463
|
+
for (const key of featureKeys) {
|
|
464
|
+
if (device && device[key] !== undefined && !shown.has(key.toLowerCase())) {
|
|
465
|
+
const line = document.createElement('div')
|
|
466
|
+
line.style.display = 'flex'
|
|
467
|
+
line.style.alignItems = 'center'
|
|
468
|
+
line.style.justifyContent = 'space-between'
|
|
469
|
+
line.style.gap = '8px'
|
|
470
|
+
line.style.padding = '2px 0'
|
|
471
|
+
|
|
472
|
+
const label = document.createElement('span')
|
|
473
|
+
label.style.fontWeight = '600'
|
|
474
|
+
label.style.minWidth = '110px'
|
|
475
|
+
label.textContent = `${key.replace(CAMELCASE_REGEX, ' $1').replace(FIRST_CHAR_REGEX, s => s.toUpperCase())}:`
|
|
476
|
+
|
|
477
|
+
const value = document.createElement('span')
|
|
478
|
+
value.style.fontFamily = 'monospace'
|
|
479
|
+
value.style.fontSize = '11px'
|
|
480
|
+
value.style.opacity = '0.9'
|
|
481
|
+
value.style.whiteSpace = 'normal'
|
|
482
|
+
value.style.overflowWrap = 'anywhere'
|
|
483
|
+
value.style.wordBreak = 'break-word'
|
|
484
|
+
value.style.textAlign = 'right'
|
|
485
|
+
value.textContent = typeof device[key] === 'object' ? JSON.stringify(device[key]) : String(device[key])
|
|
486
|
+
|
|
487
|
+
line.appendChild(label)
|
|
488
|
+
line.appendChild(value)
|
|
489
|
+
details.appendChild(line)
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return details
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export async function renderDiscoveredDevices(
|
|
497
|
+
devices: any[],
|
|
498
|
+
options: {
|
|
499
|
+
configuredIds?: Set<string>
|
|
500
|
+
selectedIds?: Set<string>
|
|
501
|
+
onToggleSelect?: (device: any, selected: boolean) => void
|
|
502
|
+
} = {},
|
|
503
|
+
): Promise<HTMLElement> {
|
|
504
|
+
const ul = document.createElement('ul')
|
|
505
|
+
ul.className = 'device-grid'
|
|
506
|
+
ul.style.maxHeight = '400px'
|
|
507
|
+
ul.style.overflowY = 'auto'
|
|
508
|
+
ul.style.marginTop = '12px'
|
|
509
|
+
ul.style.padding = '0'
|
|
510
|
+
ul.style.listStyle = 'none'
|
|
511
|
+
|
|
512
|
+
const { addDeviceToConfig } = await import('./discovery.js')
|
|
513
|
+
const { loadConfiguredDevices } = await import('./devices.js')
|
|
514
|
+
const configuredIds = options.configuredIds ?? new Set<string>()
|
|
515
|
+
const selectedIds = options.selectedIds ?? new Set<string>()
|
|
516
|
+
const onToggleSelect = options.onToggleSelect
|
|
517
|
+
|
|
518
|
+
for (const d of devices) {
|
|
519
|
+
// Defensive check: warn if device is missing id, name, or type
|
|
520
|
+
if (!d || (!d.id && !d.deviceId) || (!d.name && !d.type)) {
|
|
521
|
+
console.warn('[SwitchBot][Discovery][renderDiscoveredDevices] Device missing required fields:', d)
|
|
522
|
+
}
|
|
523
|
+
const deviceId = normalizeId(d.id)
|
|
524
|
+
const alreadyAdded = configuredIds.has(deviceId)
|
|
525
|
+
|
|
526
|
+
const li = document.createElement('li')
|
|
527
|
+
li.className = 'device-item'
|
|
528
|
+
li.style.display = 'flex'
|
|
529
|
+
li.style.flexDirection = 'column'
|
|
530
|
+
li.style.alignItems = 'stretch'
|
|
531
|
+
li.style.justifyContent = 'flex-start'
|
|
532
|
+
li.style.padding = '5px 8px'
|
|
533
|
+
li.style.marginBottom = '0'
|
|
534
|
+
li.style.borderRadius = '5px'
|
|
535
|
+
li.style.transition = 'all 0.2s ease'
|
|
536
|
+
|
|
537
|
+
const info = document.createElement('div')
|
|
538
|
+
info.style.flex = '1 1 auto'
|
|
539
|
+
info.style.width = '100%'
|
|
540
|
+
info.style.minWidth = '0'
|
|
541
|
+
|
|
542
|
+
const nameContainer = document.createElement('div')
|
|
543
|
+
nameContainer.style.display = 'flex'
|
|
544
|
+
nameContainer.style.alignItems = 'center'
|
|
545
|
+
nameContainer.style.marginBottom = '0'
|
|
546
|
+
nameContainer.style.flexWrap = 'wrap'
|
|
547
|
+
nameContainer.style.gap = '4px'
|
|
548
|
+
|
|
549
|
+
const name = document.createElement('div')
|
|
550
|
+
name.style.fontWeight = '500'
|
|
551
|
+
name.style.fontSize = '13px'
|
|
552
|
+
name.textContent = d.name || d.id
|
|
553
|
+
|
|
554
|
+
const selectCheckbox = document.createElement('input')
|
|
555
|
+
selectCheckbox.type = 'checkbox'
|
|
556
|
+
selectCheckbox.style.width = 'auto'
|
|
557
|
+
selectCheckbox.style.margin = '0 2px 0 0'
|
|
558
|
+
selectCheckbox.checked = selectedIds.has(deviceId)
|
|
559
|
+
if (alreadyAdded) {
|
|
560
|
+
selectCheckbox.disabled = true
|
|
561
|
+
selectCheckbox.title = 'Already configured'
|
|
562
|
+
}
|
|
563
|
+
selectCheckbox.onchange = () => {
|
|
564
|
+
onToggleSelect?.(d, selectCheckbox.checked)
|
|
565
|
+
// Notify listeners (e.g., batch buttons) of selection change
|
|
566
|
+
window.dispatchEvent(new CustomEvent('discovery-selection-changed'))
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
nameContainer.appendChild(selectCheckbox)
|
|
570
|
+
|
|
571
|
+
nameContainer.appendChild(name)
|
|
572
|
+
|
|
573
|
+
// Show firmware update available indicator if present
|
|
574
|
+
if (d.firmwareUpdateAvailable) {
|
|
575
|
+
const fwBadge = document.createElement('span')
|
|
576
|
+
fwBadge.textContent = 'Update Available'
|
|
577
|
+
fwBadge.style.cssText = 'background: #fb923c; color: #111; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;'
|
|
578
|
+
fwBadge.title = 'A firmware update is available for this device.'
|
|
579
|
+
nameContainer.appendChild(fwBadge)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Show offline/unreachable indicator if device is offline
|
|
583
|
+
let offline = false
|
|
584
|
+
const lastSeen = d.lastSeen || d.lastseen || d.updatedAt
|
|
585
|
+
if (typeof d.offline === 'boolean') {
|
|
586
|
+
offline = d.offline
|
|
587
|
+
} else if (lastSeen) {
|
|
588
|
+
try {
|
|
589
|
+
const last = new Date(lastSeen).getTime()
|
|
590
|
+
if (!Number.isNaN(last)) {
|
|
591
|
+
if (Date.now() - last > 1000 * 60 * 60) { // 1 hour
|
|
592
|
+
offline = true
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
} catch {}
|
|
596
|
+
}
|
|
597
|
+
if (offline) {
|
|
598
|
+
const offlineBadge = document.createElement('span')
|
|
599
|
+
offlineBadge.textContent = 'Offline'
|
|
600
|
+
offlineBadge.style.cssText = 'background: #dc2626; color: white; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;'
|
|
601
|
+
offlineBadge.title = 'Device is offline or unreachable.'
|
|
602
|
+
nameContainer.appendChild(offlineBadge)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const expandedDetails = document.createElement('div')
|
|
606
|
+
expandedDetails.style.display = 'none'
|
|
607
|
+
expandedDetails.appendChild(renderDeviceDetailsPanel(d))
|
|
608
|
+
|
|
609
|
+
const expandBtn = document.createElement('button')
|
|
610
|
+
expandBtn.textContent = '▾'
|
|
611
|
+
expandBtn.title = 'Show details'
|
|
612
|
+
expandBtn.style.padding = '2px 6px'
|
|
613
|
+
expandBtn.style.fontSize = '11px'
|
|
614
|
+
expandBtn.style.marginLeft = '4px'
|
|
615
|
+
expandBtn.style.background = '#e5e7eb'
|
|
616
|
+
expandBtn.style.color = '#111827'
|
|
617
|
+
expandBtn.style.transition = 'transform 0.2s ease'
|
|
618
|
+
expandBtn.onclick = () => {
|
|
619
|
+
const isHidden = expandedDetails.style.display === 'none'
|
|
620
|
+
expandedDetails.style.display = isHidden ? 'block' : 'none'
|
|
621
|
+
expandBtn.style.transform = isHidden ? 'rotate(180deg)' : 'rotate(0deg)'
|
|
622
|
+
}
|
|
623
|
+
nameContainer.appendChild(expandBtn)
|
|
624
|
+
|
|
625
|
+
const duplicateBadge = document.createElement('span')
|
|
626
|
+
duplicateBadge.textContent = alreadyAdded ? '✓ Already Added' : '➕ New Device'
|
|
627
|
+
duplicateBadge.style.cssText = alreadyAdded
|
|
628
|
+
? 'background: #16a34a; color: white; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600;'
|
|
629
|
+
: 'background: #2563eb; color: white; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600;'
|
|
630
|
+
nameContainer.appendChild(duplicateBadge)
|
|
631
|
+
|
|
632
|
+
// Add connection type badge
|
|
633
|
+
if (d.connectionType) {
|
|
634
|
+
const badge = renderConnectionBadge(d.connectionType)
|
|
635
|
+
if (badge) {
|
|
636
|
+
nameContainer.appendChild(badge)
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Add IR badge if it's an IR device
|
|
641
|
+
if (d.isIR) {
|
|
642
|
+
nameContainer.appendChild(renderIRBadge())
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Add signal strength visualization (only for BLE/wireless devices)
|
|
646
|
+
if (d.rssi !== undefined && d.rssi !== null && d.rssi !== 0) {
|
|
647
|
+
nameContainer.appendChild(renderSignalBars(d.rssi))
|
|
648
|
+
nameContainer.appendChild(renderSignalQualityBadge(d.rssi))
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Add battery warning indicator if battery < 20%
|
|
652
|
+
if (typeof d.battery === 'number' && d.battery < 20) {
|
|
653
|
+
const batteryWarn = document.createElement('span')
|
|
654
|
+
batteryWarn.textContent = `⚠️ ${d.battery}%`
|
|
655
|
+
batteryWarn.style.cssText
|
|
656
|
+
= d.battery < 10
|
|
657
|
+
? 'background: #dc2626; color: white; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;'
|
|
658
|
+
: 'background: #fbbf24; color: #111; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;'
|
|
659
|
+
batteryWarn.title = d.battery < 10 ? 'Battery critically low' : 'Battery low'
|
|
660
|
+
nameContainer.appendChild(batteryWarn)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Defensive check: warn if device is missing id, name, or type (for details panel)
|
|
664
|
+
if (!d || (!d.id && !d.deviceId) || (!d.name && !d.type)) {
|
|
665
|
+
console.warn('[SwitchBot][Discovery][renderDeviceDetailsPanel] Device missing required fields:', d)
|
|
666
|
+
}
|
|
667
|
+
const details = document.createElement('div')
|
|
668
|
+
details.style.fontSize = '10px'
|
|
669
|
+
details.style.opacity = '0.7'
|
|
670
|
+
details.style.marginTop = '0'
|
|
671
|
+
details.style.fontFamily = 'monospace'
|
|
672
|
+
details.style.whiteSpace = 'normal'
|
|
673
|
+
details.style.overflowWrap = 'anywhere'
|
|
674
|
+
details.style.wordBreak = 'break-word'
|
|
675
|
+
|
|
676
|
+
let detailsText = `ID: ${d.id} | Type: ${d.type} | Model: ${d.model || 'N/A'}`
|
|
677
|
+
if (d.hubDeviceId) {
|
|
678
|
+
detailsText += ` | Hub: ${d.hubDeviceId}`
|
|
679
|
+
}
|
|
680
|
+
if (d.address) {
|
|
681
|
+
detailsText += ` | MAC: ${d.address}`
|
|
682
|
+
}
|
|
683
|
+
details.textContent = detailsText
|
|
684
|
+
|
|
685
|
+
info.appendChild(nameContainer)
|
|
686
|
+
info.appendChild(details)
|
|
687
|
+
info.appendChild(expandedDetails)
|
|
688
|
+
|
|
689
|
+
const addBtn = document.createElement('button')
|
|
690
|
+
addBtn.textContent = alreadyAdded ? 'Already Added' : 'Add to Config'
|
|
691
|
+
addBtn.style.marginLeft = '0'
|
|
692
|
+
addBtn.style.marginTop = '2px'
|
|
693
|
+
addBtn.style.padding = '4px 9px'
|
|
694
|
+
addBtn.style.fontSize = '11px'
|
|
695
|
+
addBtn.style.whiteSpace = 'nowrap'
|
|
696
|
+
addBtn.style.flexShrink = '0'
|
|
697
|
+
addBtn.disabled = alreadyAdded
|
|
698
|
+
if (alreadyAdded) {
|
|
699
|
+
addBtn.style.opacity = '0.65'
|
|
700
|
+
addBtn.style.cursor = 'not-allowed'
|
|
701
|
+
addBtn.style.background = '#6b7280'
|
|
702
|
+
}
|
|
703
|
+
addBtn.onclick = async () => {
|
|
704
|
+
if (alreadyAdded) {
|
|
705
|
+
return
|
|
706
|
+
}
|
|
707
|
+
await addDeviceToConfig(d)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (alreadyAdded) {
|
|
711
|
+
const viewBtn = document.createElement('button')
|
|
712
|
+
viewBtn.textContent = 'View in Config'
|
|
713
|
+
viewBtn.className = 'secondary'
|
|
714
|
+
viewBtn.style.marginLeft = '0'
|
|
715
|
+
viewBtn.style.padding = '4px 9px'
|
|
716
|
+
viewBtn.style.fontSize = '11px'
|
|
717
|
+
viewBtn.onclick = async () => {
|
|
718
|
+
await loadConfiguredDevices()
|
|
719
|
+
scrollToConfiguredDevice(d.id)
|
|
720
|
+
}
|
|
721
|
+
li.appendChild(info)
|
|
722
|
+
const actions = document.createElement('div')
|
|
723
|
+
actions.className = 'device-actions'
|
|
724
|
+
actions.style.display = 'flex'
|
|
725
|
+
actions.style.alignItems = 'center'
|
|
726
|
+
actions.style.flexWrap = 'wrap'
|
|
727
|
+
actions.style.justifyContent = 'flex-start'
|
|
728
|
+
actions.style.marginLeft = '0'
|
|
729
|
+
actions.style.width = '100%'
|
|
730
|
+
actions.style.marginTop = '2px'
|
|
731
|
+
actions.style.gap = '5px'
|
|
732
|
+
actions.appendChild(viewBtn)
|
|
733
|
+
actions.appendChild(addBtn)
|
|
734
|
+
actions.appendChild(createConnectionTestControls(d))
|
|
735
|
+
li.appendChild(actions)
|
|
736
|
+
ul.appendChild(li)
|
|
737
|
+
continue
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const actions = document.createElement('div')
|
|
741
|
+
actions.className = 'device-actions'
|
|
742
|
+
actions.style.display = 'flex'
|
|
743
|
+
actions.style.flexWrap = 'wrap'
|
|
744
|
+
actions.style.justifyContent = 'flex-start'
|
|
745
|
+
actions.style.marginLeft = '0'
|
|
746
|
+
actions.style.width = '100%'
|
|
747
|
+
actions.style.marginTop = '2px'
|
|
748
|
+
actions.style.gap = '5px'
|
|
749
|
+
actions.appendChild(addBtn)
|
|
750
|
+
actions.appendChild(createConnectionTestControls(d))
|
|
751
|
+
|
|
752
|
+
li.appendChild(info)
|
|
753
|
+
li.appendChild(actions)
|
|
754
|
+
ul.appendChild(li)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return ul
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Filter devices by connection type and search query
|
|
762
|
+
* @param devices Discovered devices array
|
|
763
|
+
* @param connectionType Filter: 'all' | 'ble' | 'api' | 'both' | 'ir'
|
|
764
|
+
* @param searchQuery Search term to match against name/id/type
|
|
765
|
+
* @returns Filtered devices array
|
|
766
|
+
*/
|
|
767
|
+
export function filterDevices(
|
|
768
|
+
devices: any[],
|
|
769
|
+
connectionType: 'all' | 'ble' | 'api' | 'both' | 'ir' = 'all',
|
|
770
|
+
searchQuery = '',
|
|
771
|
+
): any[] {
|
|
772
|
+
let filtered = [...devices]
|
|
773
|
+
|
|
774
|
+
// Filter by connection type
|
|
775
|
+
if (connectionType !== 'all') {
|
|
776
|
+
filtered = filtered.filter((d) => {
|
|
777
|
+
if (connectionType === 'ir') {
|
|
778
|
+
return d.isIR === true
|
|
779
|
+
}
|
|
780
|
+
if (connectionType === 'ble') {
|
|
781
|
+
return d.connectionType === 'BLE' || d.connectionType?.includes('BLE')
|
|
782
|
+
}
|
|
783
|
+
if (connectionType === 'api') {
|
|
784
|
+
return d.connectionType === 'OpenAPI' || d.connectionType === 'API' || d.connectionType?.includes('API')
|
|
785
|
+
}
|
|
786
|
+
if (connectionType === 'both') {
|
|
787
|
+
return d.connectionType === 'Both' || d.connectionType?.includes('Both')
|
|
788
|
+
}
|
|
789
|
+
return true
|
|
790
|
+
})
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Filter by search query
|
|
794
|
+
if (searchQuery.trim()) {
|
|
795
|
+
const query = searchQuery.toLowerCase()
|
|
796
|
+
filtered = filtered.filter((d) => {
|
|
797
|
+
const name = (d.name || '').toLowerCase()
|
|
798
|
+
const id = (d.id || '').toLowerCase()
|
|
799
|
+
const type = (d.type || '').toLowerCase()
|
|
800
|
+
const model = (d.model || '').toLowerCase()
|
|
801
|
+
return name.includes(query) || id.includes(query) || type.includes(query) || model.includes(query)
|
|
802
|
+
})
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return filtered
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Sort devices by specified criteria
|
|
810
|
+
* @param devices Devices array to sort
|
|
811
|
+
* @param sortBy Sort criterion: 'name' | 'signal' | 'type' | 'connection'
|
|
812
|
+
* @returns Sorted devices array
|
|
813
|
+
*/
|
|
814
|
+
export function sortDevices(
|
|
815
|
+
devices: any[],
|
|
816
|
+
sortBy: 'name' | 'signal' | 'type' | 'connection' = 'name',
|
|
817
|
+
): any[] {
|
|
818
|
+
const sorted = [...devices]
|
|
819
|
+
|
|
820
|
+
switch (sortBy) {
|
|
821
|
+
case 'signal': {
|
|
822
|
+
// Sort by RSSI descending (strongest signal first)
|
|
823
|
+
sorted.sort((a, b) => {
|
|
824
|
+
const aRssi = a.rssi || 0
|
|
825
|
+
const bRssi = b.rssi || 0
|
|
826
|
+
return bRssi - aRssi // Descending order (higher is stronger)
|
|
827
|
+
})
|
|
828
|
+
break
|
|
829
|
+
}
|
|
830
|
+
case 'type': {
|
|
831
|
+
// Sort by device type alphabetically
|
|
832
|
+
sorted.sort((a, b) => {
|
|
833
|
+
const aType = (a.type || '').localeCompare(b.type || '')
|
|
834
|
+
return aType
|
|
835
|
+
})
|
|
836
|
+
break
|
|
837
|
+
}
|
|
838
|
+
case 'connection': {
|
|
839
|
+
// Sort by connection type: Both > BLE > OpenAPI > Others
|
|
840
|
+
const connectionOrder: Record<string, number> = {
|
|
841
|
+
Both: 0,
|
|
842
|
+
BLE: 1,
|
|
843
|
+
OpenAPI: 2,
|
|
844
|
+
API: 2,
|
|
845
|
+
Unknown: 3,
|
|
846
|
+
}
|
|
847
|
+
sorted.sort((a, b) => {
|
|
848
|
+
const aOrder = connectionOrder[a.connectionType || 'Unknown'] ?? 3
|
|
849
|
+
const bOrder = connectionOrder[b.connectionType || 'Unknown'] ?? 3
|
|
850
|
+
return aOrder - bOrder
|
|
851
|
+
})
|
|
852
|
+
break
|
|
853
|
+
}
|
|
854
|
+
case 'name':
|
|
855
|
+
default: {
|
|
856
|
+
// Sort by name alphabetically
|
|
857
|
+
sorted.sort((a, b) => (a.name || a.id || '').localeCompare(b.name || b.id || ''))
|
|
858
|
+
break
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return sorted
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Get filter/sort preferences from localStorage
|
|
867
|
+
* @returns Object with current filter and sort preferences
|
|
868
|
+
*/
|
|
869
|
+
export function getDiscoveryPreferences(): {
|
|
870
|
+
connectionType: 'all' | 'ble' | 'api' | 'both' | 'ir'
|
|
871
|
+
sortBy: 'name' | 'signal' | 'type' | 'connection'
|
|
872
|
+
searchQuery: string
|
|
873
|
+
} {
|
|
874
|
+
try {
|
|
875
|
+
const stored = localStorage.getItem('discoveryPreferences')
|
|
876
|
+
if (stored) {
|
|
877
|
+
return JSON.parse(stored)
|
|
878
|
+
}
|
|
879
|
+
} catch (_e) {
|
|
880
|
+
// Ignore parse errors
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
return {
|
|
884
|
+
connectionType: 'all',
|
|
885
|
+
sortBy: 'name',
|
|
886
|
+
searchQuery: '',
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Save filter/sort preferences to localStorage
|
|
892
|
+
* @param preferences Preferences object to save
|
|
893
|
+
* @param preferences.connectionType Connection type filter
|
|
894
|
+
* @param preferences.sortBy Sort criterion
|
|
895
|
+
* @param preferences.searchQuery Search query string
|
|
896
|
+
*/
|
|
897
|
+
export function setDiscoveryPreferences(preferences: {
|
|
898
|
+
connectionType: 'all' | 'ble' | 'api' | 'both' | 'ir'
|
|
899
|
+
sortBy: 'name' | 'signal' | 'type' | 'connection'
|
|
900
|
+
searchQuery: string
|
|
901
|
+
}): void {
|
|
902
|
+
try {
|
|
903
|
+
localStorage.setItem('discoveryPreferences', JSON.stringify(preferences))
|
|
904
|
+
} catch (_e) {
|
|
905
|
+
// Ignore storage errors
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
export function renderDeviceList(list: any[]): void {
|
|
910
|
+
const ul = document.getElementById('devices')
|
|
911
|
+
const status = document.getElementById('status')
|
|
912
|
+
const removeAllContainer = document.getElementById('removeAllContainer')
|
|
913
|
+
|
|
914
|
+
if (!ul || !status) {
|
|
915
|
+
return
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (!list.length) {
|
|
919
|
+
status.textContent = 'No devices found in config.'
|
|
920
|
+
ul.innerHTML = ''
|
|
921
|
+
// Hide remove all button when no devices
|
|
922
|
+
if (removeAllContainer) {
|
|
923
|
+
removeAllContainer.style.display = 'none'
|
|
924
|
+
}
|
|
925
|
+
return
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
status.textContent = `Found ${list.length} device(s)`
|
|
929
|
+
ul.classList.add('device-grid')
|
|
930
|
+
ul.innerHTML = ''
|
|
931
|
+
|
|
932
|
+
// Show remove all button when devices exist
|
|
933
|
+
if (removeAllContainer) {
|
|
934
|
+
removeAllContainer.style.display = 'block'
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
for (const d of list) {
|
|
938
|
+
const li = document.createElement('li')
|
|
939
|
+
li.className = 'device-item'
|
|
940
|
+
li.setAttribute('data-device-id', normalizeId(d.id))
|
|
941
|
+
li.style.display = 'flex'
|
|
942
|
+
li.style.flexDirection = 'column'
|
|
943
|
+
li.style.alignItems = 'stretch'
|
|
944
|
+
li.style.marginBottom = '0'
|
|
945
|
+
|
|
946
|
+
const info = document.createElement('div')
|
|
947
|
+
info.style.flex = '1 1 auto'
|
|
948
|
+
info.style.width = '100%'
|
|
949
|
+
info.style.minWidth = '0'
|
|
950
|
+
|
|
951
|
+
const nameContainer = document.createElement('div')
|
|
952
|
+
nameContainer.style.display = 'flex'
|
|
953
|
+
nameContainer.style.flexDirection = 'column'
|
|
954
|
+
nameContainer.style.alignItems = 'flex-start'
|
|
955
|
+
nameContainer.style.gap = '0'
|
|
956
|
+
|
|
957
|
+
const name = document.createElement('div')
|
|
958
|
+
name.style.fontWeight = '500'
|
|
959
|
+
name.style.fontSize = '13px'
|
|
960
|
+
name.textContent = d.name || d.id
|
|
961
|
+
|
|
962
|
+
const expandedDetails = document.createElement('div')
|
|
963
|
+
expandedDetails.style.display = 'none'
|
|
964
|
+
expandedDetails.appendChild(renderDeviceDetailsPanel(d))
|
|
965
|
+
|
|
966
|
+
const expandBtn = document.createElement('button')
|
|
967
|
+
expandBtn.textContent = '▾'
|
|
968
|
+
expandBtn.title = 'Show details'
|
|
969
|
+
expandBtn.style.padding = '2px 6px'
|
|
970
|
+
expandBtn.style.fontSize = '11px'
|
|
971
|
+
expandBtn.style.marginLeft = '4px'
|
|
972
|
+
expandBtn.style.background = '#e5e7eb'
|
|
973
|
+
expandBtn.style.color = '#111827'
|
|
974
|
+
expandBtn.style.transition = 'transform 0.2s ease'
|
|
975
|
+
expandBtn.onclick = () => {
|
|
976
|
+
const isHidden = expandedDetails.style.display === 'none'
|
|
977
|
+
expandedDetails.style.display = isHidden ? 'block' : 'none'
|
|
978
|
+
expandBtn.style.transform = isHidden ? 'rotate(180deg)' : 'rotate(0deg)'
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const code = document.createElement('code')
|
|
982
|
+
code.textContent = d.id
|
|
983
|
+
code.style.fontSize = '10px'
|
|
984
|
+
code.style.opacity = '0.75'
|
|
985
|
+
code.style.marginLeft = '0'
|
|
986
|
+
code.style.whiteSpace = 'normal'
|
|
987
|
+
code.style.overflowWrap = 'anywhere'
|
|
988
|
+
code.style.wordBreak = 'break-word'
|
|
989
|
+
code.style.maxWidth = '100%'
|
|
990
|
+
|
|
991
|
+
const headerRow = document.createElement('div')
|
|
992
|
+
headerRow.style.display = 'inline-flex'
|
|
993
|
+
headerRow.style.alignItems = 'center'
|
|
994
|
+
headerRow.style.gap = '4px'
|
|
995
|
+
headerRow.appendChild(name)
|
|
996
|
+
headerRow.appendChild(expandBtn)
|
|
997
|
+
|
|
998
|
+
nameContainer.appendChild(headerRow)
|
|
999
|
+
nameContainer.appendChild(code)
|
|
1000
|
+
|
|
1001
|
+
// Add signal strength visualization if RSSI is available
|
|
1002
|
+
if (d.rssi !== undefined && d.rssi !== null && d.rssi !== 0) {
|
|
1003
|
+
nameContainer.appendChild(renderSignalBars(d.rssi))
|
|
1004
|
+
nameContainer.appendChild(renderSignalQualityBadge(d.rssi))
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const meta = document.createElement('div')
|
|
1008
|
+
meta.style.opacity = '0.75'
|
|
1009
|
+
meta.style.marginTop = '0'
|
|
1010
|
+
meta.style.fontSize = '11px'
|
|
1011
|
+
|
|
1012
|
+
const typeText = d.type ? `type: ${d.type}` : ''
|
|
1013
|
+
const connText = d.connectionPreference ? `conn: ${d.connectionPreference}` : ''
|
|
1014
|
+
const roomText = d.room ? `room: ${d.room}` : ''
|
|
1015
|
+
meta.textContent = [typeText, connText, roomText].filter(Boolean).join(' | ')
|
|
1016
|
+
|
|
1017
|
+
info.appendChild(nameContainer)
|
|
1018
|
+
info.appendChild(meta)
|
|
1019
|
+
info.appendChild(expandedDetails)
|
|
1020
|
+
|
|
1021
|
+
const buttons = document.createElement('div')
|
|
1022
|
+
buttons.className = 'device-actions'
|
|
1023
|
+
buttons.style.display = 'flex'
|
|
1024
|
+
buttons.style.flexWrap = 'wrap'
|
|
1025
|
+
buttons.style.justifyContent = 'flex-start'
|
|
1026
|
+
buttons.style.marginLeft = '0'
|
|
1027
|
+
buttons.style.width = '100%'
|
|
1028
|
+
buttons.style.marginTop = '2px'
|
|
1029
|
+
buttons.style.gap = '5px'
|
|
1030
|
+
|
|
1031
|
+
const editBtn = document.createElement('button')
|
|
1032
|
+
editBtn.textContent = '✏️ Edit'
|
|
1033
|
+
editBtn.style.padding = '4px 9px'
|
|
1034
|
+
editBtn.style.fontSize = '11px'
|
|
1035
|
+
editBtn.onclick = async () => {
|
|
1036
|
+
const { editDevice } = await import('./modals.js')
|
|
1037
|
+
await editDevice(d)
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const copyBtn = document.createElement('button')
|
|
1041
|
+
copyBtn.textContent = 'Copy ID'
|
|
1042
|
+
copyBtn.style.padding = '4px 9px'
|
|
1043
|
+
copyBtn.style.fontSize = '11px'
|
|
1044
|
+
copyBtn.addEventListener('click', async () => {
|
|
1045
|
+
try {
|
|
1046
|
+
await navigator.clipboard.writeText(d.id)
|
|
1047
|
+
copyBtn.textContent = 'Copied'
|
|
1048
|
+
copyBtn.classList.add('success')
|
|
1049
|
+
setTimeout(() => {
|
|
1050
|
+
copyBtn.textContent = 'Copy ID'
|
|
1051
|
+
copyBtn.classList.remove('success')
|
|
1052
|
+
}, 1200)
|
|
1053
|
+
} catch (e) {
|
|
1054
|
+
copyBtn.textContent = 'Failed'
|
|
1055
|
+
copyBtn.classList.add('error')
|
|
1056
|
+
setTimeout(() => {
|
|
1057
|
+
copyBtn.textContent = 'Copy ID'
|
|
1058
|
+
copyBtn.classList.remove('error')
|
|
1059
|
+
}, 1200)
|
|
1060
|
+
}
|
|
1061
|
+
})
|
|
1062
|
+
|
|
1063
|
+
const deleteBtn = document.createElement('button')
|
|
1064
|
+
deleteBtn.textContent = '🗑️ Delete'
|
|
1065
|
+
deleteBtn.style.padding = '4px 9px'
|
|
1066
|
+
deleteBtn.style.fontSize = '11px'
|
|
1067
|
+
deleteBtn.style.background = '#ef4444'
|
|
1068
|
+
deleteBtn.onclick = async () => {
|
|
1069
|
+
const { deleteDeviceFromConfig } = await import('./devices-delete.js')
|
|
1070
|
+
await deleteDeviceFromConfig(d.id || d.deviceId, d.name || d.id || d.deviceId)
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
buttons.appendChild(editBtn)
|
|
1074
|
+
buttons.appendChild(copyBtn)
|
|
1075
|
+
buttons.appendChild(createConnectionTestControls(d))
|
|
1076
|
+
buttons.appendChild(deleteBtn)
|
|
1077
|
+
|
|
1078
|
+
li.appendChild(info)
|
|
1079
|
+
li.appendChild(buttons)
|
|
1080
|
+
ul.appendChild(li)
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// No return value needed for void function
|
|
1084
|
+
}
|