@switchbot/homebridge-switchbot
Version:
The SwitchBot plugin allows you to access your SwitchBot device(s) from HomeKit.
766 lines (690 loc) • 30.1 kB
text/typescript
import { DEVICE_TYPES } from './constants.js'
import { uiLog } from './logger.js'
export type ImportDiscoveredDeviceResult = {
configDeviceName: string
configDeviceType: string
address?: string
connectionPreference?: string
room?: string
encryptionKey?: string
keyId?: string
refreshRate?: number
blePollingEnabled?: boolean
blePollIntervalMs?: number
} | null
export async function importDiscoveredDevice(device: any): Promise<ImportDiscoveredDeviceResult> {
// --- OpenAPI Polling Interval (refreshRate) ---
const openApiRefreshLabel = document.createElement('label')
openApiRefreshLabel.textContent = 'OpenAPI Polling Interval (seconds)'
openApiRefreshLabel.style.display = 'block'
openApiRefreshLabel.style.marginBottom = '6px'
openApiRefreshLabel.style.fontWeight = '500'
openApiRefreshLabel.style.fontSize = '12px'
openApiRefreshLabel.style.color = '#6b7280'
openApiRefreshLabel.title = 'How often to poll this device via OpenAPI for status (in seconds). Overrides platform value if set. Default: 300 (5 minutes). Minimum: 30.'
const openApiRefreshInput = document.createElement('input')
openApiRefreshInput.type = 'number'
openApiRefreshInput.value = device.refreshRate || 300
openApiRefreshInput.min = '30'
openApiRefreshInput.step = '1'
openApiRefreshInput.style.width = '100%'
openApiRefreshInput.style.marginBottom = '12px'
openApiRefreshInput.style.padding = '8px 10px'
openApiRefreshInput.style.borderRadius = '6px'
openApiRefreshInput.style.fontSize = '14px'
openApiRefreshInput.style.boxSizing = 'border-box'
return new Promise((resolve) => {
const div = document.createElement('div')
div.style.position = 'fixed'
div.style.top = '0'
div.style.left = '0'
div.style.width = '100%'
div.style.height = '100%'
div.style.background = 'rgba(0,0,0,0.7)'
div.style.display = 'flex'
div.style.alignItems = 'center'
div.style.justifyContent = 'center'
div.style.zIndex = '9999'
const modal = document.createElement('div')
modal.style.background = getComputedStyle(document.body).backgroundColor
modal.style.color = getComputedStyle(document.body).color
modal.style.padding = '0'
modal.style.borderRadius = '10px'
modal.style.minWidth = '440px'
modal.style.maxWidth = '90vw'
modal.style.boxShadow = '0 8px 32px rgba(0,0,0,0.35)'
modal.style.overflow = 'hidden'
modal.style.borderTop = '3px solid var(--switchbot-red, #ef4444)'
const title = document.createElement('h3')
title.textContent = 'Import Discovered Device'
title.style.marginTop = '0'
title.style.marginBottom = '16px'
title.style.padding = '20px 20px 0'
title.style.fontSize = '18px'
title.style.fontWeight = '600'
title.style.color = 'var(--switchbot-red, #ef4444)'
title.style.letterSpacing = '-0.02em'
const contentDiv = document.createElement('div')
contentDiv.style.padding = '0 20px 20px'
const nameLabel = document.createElement('label')
nameLabel.textContent = 'Device Name'
nameLabel.style.display = 'block'
nameLabel.style.marginBottom = '6px'
nameLabel.style.fontWeight = '500'
nameLabel.style.fontSize = '12px'
nameLabel.style.color = '#6b7280'
const nameInput = document.createElement('input')
nameInput.type = 'text'
// Never allow 'undefined' as a name
let safeName = device.name
if (!safeName || safeName === 'undefined') {
safeName = device.id || ''
}
nameInput.value = safeName
nameInput.style.width = '100%'
nameInput.style.marginBottom = '12px'
nameInput.style.padding = '8px 10px'
nameInput.style.borderRadius = '6px'
nameInput.style.fontSize = '14px'
nameInput.style.boxSizing = 'border-box'
// --- Device Type Select ---
const typeLabel = document.createElement('label')
typeLabel.textContent = 'Config Device Type'
typeLabel.style.display = 'block'
typeLabel.style.marginBottom = '6px'
typeLabel.style.fontWeight = '500'
typeLabel.style.fontSize = '12px'
typeLabel.style.color = '#6b7280'
const typeSelect = document.createElement('select')
typeSelect.style.width = '100%'
typeSelect.style.padding = '8px 10px'
typeSelect.style.marginBottom = '12px'
typeSelect.style.borderRadius = '6px'
typeSelect.style.fontSize = '14px'
typeSelect.style.background = getComputedStyle(nameInput).background
typeSelect.style.color = getComputedStyle(nameInput).color
typeSelect.style.border = getComputedStyle(nameInput).border
typeSelect.style.boxSizing = 'border-box'
Object.keys(DEVICE_TYPES).forEach((categoryName) => {
const optgroup = document.createElement('optgroup')
optgroup.label = categoryName
DEVICE_TYPES[categoryName as keyof typeof DEVICE_TYPES].forEach((deviceType) => {
const opt = document.createElement('option')
opt.value = deviceType
opt.text = deviceType
const detectedType = (device.type || '').toLowerCase()
opt.selected = deviceType.toLowerCase() === detectedType
optgroup.appendChild(opt)
})
typeSelect.appendChild(optgroup)
})
// --- Connection Preference ---
const connectionPrefLabel = document.createElement('label')
connectionPrefLabel.textContent = 'Connection Preference'
connectionPrefLabel.style.display = 'block'
connectionPrefLabel.style.marginBottom = '6px'
connectionPrefLabel.style.fontWeight = '500'
connectionPrefLabel.style.fontSize = '12px'
connectionPrefLabel.style.color = '#6b7280'
const connectionPrefSelect = document.createElement('select')
connectionPrefSelect.style.width = '100%'
connectionPrefSelect.style.marginBottom = '12px'
connectionPrefSelect.style.padding = '8px 10px'
connectionPrefSelect.style.borderRadius = '6px'
connectionPrefSelect.style.fontSize = '14px'
connectionPrefSelect.style.boxSizing = 'border-box';
['auto', 'ble', 'openapi'].forEach((val) => {
const opt = document.createElement('option')
opt.value = val
opt.text = val.charAt(0).toUpperCase() + val.slice(1)
opt.selected = (device.connectionPreference || 'auto') === val
connectionPrefSelect.appendChild(opt)
})
// --- Room ---
const roomLabel = document.createElement('label')
roomLabel.textContent = 'Room'
roomLabel.style.display = 'block'
roomLabel.style.marginBottom = '6px'
roomLabel.style.fontWeight = '500'
roomLabel.style.fontSize = '12px'
roomLabel.style.color = '#6b7280'
const roomInput = document.createElement('input')
roomInput.type = 'text'
roomInput.value = device.room || ''
roomInput.placeholder = 'Optional room/location metadata'
roomInput.style.width = '100%'
roomInput.style.marginBottom = '12px'
roomInput.style.padding = '8px 10px'
roomInput.style.borderRadius = '6px'
roomInput.style.fontSize = '14px'
roomInput.style.boxSizing = 'border-box'
// --- BLE MAC Address ---
const macLabel = document.createElement('label')
macLabel.textContent = 'BLE MAC Address (optional)'
macLabel.style.display = 'block'
macLabel.style.marginBottom = '6px'
macLabel.style.fontWeight = '500'
macLabel.style.fontSize = '12px'
macLabel.style.color = '#6b7280'
const macInput = document.createElement('input')
macInput.type = 'text'
macInput.value = device.address || ''
macInput.placeholder = 'AA:BB:CC:DD:EE:FF'
macInput.style.width = '100%'
macInput.style.marginBottom = '12px'
macInput.style.padding = '8px 10px'
macInput.style.borderRadius = '6px'
macInput.style.fontSize = '14px'
macInput.style.boxSizing = 'border-box'
// --- Encryption Key ---
const encryptionKeyLabel = document.createElement('label')
encryptionKeyLabel.textContent = 'BLE Encryption Key (optional)'
encryptionKeyLabel.style.display = 'block'
encryptionKeyLabel.style.marginBottom = '6px'
encryptionKeyLabel.style.fontWeight = '500'
encryptionKeyLabel.style.fontSize = '12px'
encryptionKeyLabel.style.color = '#6b7280'
const encryptionKeyInput = document.createElement('input')
encryptionKeyInput.type = 'password'
encryptionKeyInput.value = device.encryptionKey || ''
encryptionKeyInput.placeholder = 'Paste device BLE encryption key'
encryptionKeyInput.style.width = '100%'
encryptionKeyInput.style.marginBottom = '12px'
encryptionKeyInput.style.padding = '8px 10px'
encryptionKeyInput.style.borderRadius = '6px'
encryptionKeyInput.style.fontSize = '14px'
encryptionKeyInput.style.boxSizing = 'border-box'
// --- Key ID ---
const keyIdLabel = document.createElement('label')
keyIdLabel.textContent = 'BLE Key ID (optional)'
keyIdLabel.style.display = 'block'
keyIdLabel.style.marginBottom = '6px'
keyIdLabel.style.fontWeight = '500'
keyIdLabel.style.fontSize = '12px'
keyIdLabel.style.color = '#6b7280'
const keyIdInput = document.createElement('input')
keyIdInput.type = 'text'
keyIdInput.value = device.keyId || ''
keyIdInput.placeholder = 'e.g. ff'
keyIdInput.style.width = '100%'
keyIdInput.style.marginBottom = '12px'
keyIdInput.style.padding = '8px 10px'
keyIdInput.style.borderRadius = '6px'
keyIdInput.style.fontSize = '14px'
keyIdInput.style.boxSizing = 'border-box'
// --- BLE Polling Enabled ---
const blePollingEnabledLabel = document.createElement('label')
blePollingEnabledLabel.textContent = 'Enable BLE Polling Fallback'
blePollingEnabledLabel.style.display = 'block'
blePollingEnabledLabel.style.marginBottom = '6px'
blePollingEnabledLabel.style.fontWeight = '500'
blePollingEnabledLabel.style.fontSize = '12px'
blePollingEnabledLabel.style.color = '#6b7280'
const blePollingEnabledInput = document.createElement('input')
blePollingEnabledInput.type = 'checkbox'
blePollingEnabledInput.checked = device.blePollingEnabled !== false // default true
blePollingEnabledInput.style.marginRight = '8px'
blePollingEnabledInput.style.marginBottom = '12px'
// --- BLE Poll Interval ---
const blePollIntervalLabel = document.createElement('label')
blePollIntervalLabel.textContent = 'BLE Polling Interval (ms)'
blePollIntervalLabel.style.display = 'block'
blePollIntervalLabel.style.marginBottom = '6px'
blePollIntervalLabel.style.fontWeight = '500'
blePollIntervalLabel.style.fontSize = '12px'
blePollIntervalLabel.style.color = '#6b7280'
const blePollIntervalInput = document.createElement('input')
blePollIntervalInput.type = 'number'
blePollIntervalInput.value = device.blePollIntervalMs || 600000
blePollIntervalInput.min = '60000'
blePollIntervalInput.step = '1000'
blePollIntervalInput.style.width = '100%'
blePollIntervalInput.style.marginBottom = '12px'
blePollIntervalInput.style.padding = '8px 10px'
blePollIntervalInput.style.borderRadius = '6px'
blePollIntervalInput.style.fontSize = '14px'
blePollIntervalInput.style.boxSizing = 'border-box'
const buttons = document.createElement('div')
buttons.style.display = 'flex'
buttons.style.gap = '10px'
buttons.style.justifyContent = 'flex-end'
buttons.style.marginTop = '18px'
buttons.style.paddingTop = '18px'
buttons.style.borderTop = '1px solid rgba(0, 0, 0, 0.08)'
const cancelBtn = document.createElement('button')
cancelBtn.textContent = 'Cancel'
cancelBtn.className = 'secondary'
cancelBtn.style.background = '#6b7280'
cancelBtn.style.padding = '8px 16px'
cancelBtn.style.fontSize = '13px'
const importBtn = document.createElement('button')
importBtn.textContent = 'Add to Config'
importBtn.style.background = 'var(--switchbot-red, #ef4444)'
importBtn.style.padding = '8px 20px'
importBtn.style.fontSize = '13px'
const cleanup = (result: ImportDiscoveredDeviceResult) => {
div.remove()
resolve(result)
}
cancelBtn.onclick = () => cleanup(null)
importBtn.onclick = () => {
// Never allow 'undefined' as a name
let finalName = nameInput.value
if (!finalName || finalName === 'undefined') {
finalName = device.id || ''
}
cleanup({
configDeviceName: finalName,
configDeviceType: typeSelect.value || device.type,
address: macInput.value || undefined,
connectionPreference: connectionPrefSelect.value || undefined,
room: roomInput.value || undefined,
encryptionKey: encryptionKeyInput.value || undefined,
keyId: keyIdInput.value || undefined,
refreshRate: Number(openApiRefreshInput.value) || 300,
blePollingEnabled: blePollingEnabledInput.checked,
blePollIntervalMs: Number(blePollIntervalInput.value) || 600000,
})
}
div.addEventListener('click', (event) => {
if (event.target === div) {
cleanup(null)
}
})
buttons.appendChild(cancelBtn)
buttons.appendChild(importBtn)
contentDiv.appendChild(nameLabel)
contentDiv.appendChild(nameInput)
contentDiv.appendChild(typeLabel)
contentDiv.appendChild(typeSelect)
contentDiv.appendChild(connectionPrefLabel)
contentDiv.appendChild(connectionPrefSelect)
contentDiv.appendChild(roomLabel)
contentDiv.appendChild(roomInput)
contentDiv.appendChild(macLabel)
contentDiv.appendChild(macInput)
contentDiv.appendChild(encryptionKeyLabel)
contentDiv.appendChild(encryptionKeyInput)
contentDiv.appendChild(keyIdLabel)
contentDiv.appendChild(keyIdInput)
contentDiv.appendChild(openApiRefreshLabel)
contentDiv.appendChild(openApiRefreshInput)
contentDiv.appendChild(blePollingEnabledLabel)
contentDiv.appendChild(blePollingEnabledInput)
contentDiv.appendChild(blePollIntervalLabel)
contentDiv.appendChild(blePollIntervalInput)
contentDiv.appendChild(buttons)
modal.appendChild(title)
modal.appendChild(contentDiv)
div.appendChild(modal)
document.body.appendChild(div)
nameInput.focus()
})
}
export async function editDevice(device: any): Promise<void> {
// --- OpenAPI Polling Interval (refreshRate) ---
const openApiRefreshLabel = document.createElement('label')
openApiRefreshLabel.textContent = 'OpenAPI Polling Interval (seconds)'
openApiRefreshLabel.style.display = 'block'
openApiRefreshLabel.style.marginBottom = '6px'
openApiRefreshLabel.style.fontWeight = '500'
openApiRefreshLabel.style.fontSize = '12px'
openApiRefreshLabel.style.color = '#6b7280'
openApiRefreshLabel.title = 'How often to poll this device via OpenAPI for status (in seconds). Overrides platform value if set. Default: 300 (5 minutes). Minimum: 30.'
const openApiRefreshInput = document.createElement('input')
openApiRefreshInput.type = 'number'
openApiRefreshInput.value = device.refreshRate || 300
openApiRefreshInput.min = '30'
openApiRefreshInput.step = '1'
openApiRefreshInput.style.width = '100%'
openApiRefreshInput.style.marginBottom = '12px'
openApiRefreshInput.style.padding = '8px 10px'
openApiRefreshInput.style.borderRadius = '6px'
openApiRefreshInput.style.fontSize = '14px'
openApiRefreshInput.style.boxSizing = 'border-box'
// --- Device Type Select ---
// --- BLE Polling Enabled ---
const blePollingEnabledLabel = document.createElement('label')
blePollingEnabledLabel.textContent = 'Enable BLE Polling Fallback'
blePollingEnabledLabel.style.display = 'block'
blePollingEnabledLabel.style.marginBottom = '6px'
blePollingEnabledLabel.style.fontWeight = '500'
blePollingEnabledLabel.style.fontSize = '12px'
blePollingEnabledLabel.style.color = '#6b7280'
const blePollingEnabledInput = document.createElement('input')
blePollingEnabledInput.type = 'checkbox'
blePollingEnabledInput.checked = device.blePollingEnabled !== false // default true
blePollingEnabledInput.style.marginRight = '8px'
blePollingEnabledInput.style.marginBottom = '12px'
// --- BLE Poll Interval ---
const blePollIntervalLabel = document.createElement('label')
blePollIntervalLabel.textContent = 'BLE Polling Interval (ms)'
blePollIntervalLabel.style.display = 'block'
blePollIntervalLabel.style.marginBottom = '6px'
blePollIntervalLabel.style.fontWeight = '500'
blePollIntervalLabel.style.fontSize = '12px'
blePollIntervalLabel.style.color = '#6b7280'
const blePollIntervalInput = document.createElement('input')
blePollIntervalInput.type = 'number'
blePollIntervalInput.value = device.blePollIntervalMs || 600000
blePollIntervalInput.min = '60000'
blePollIntervalInput.step = '1000'
blePollIntervalInput.style.width = '100%'
blePollIntervalInput.style.marginBottom = '12px'
blePollIntervalInput.style.padding = '8px 10px'
blePollIntervalInput.style.borderRadius = '6px'
blePollIntervalInput.style.fontSize = '14px'
blePollIntervalInput.style.boxSizing = 'border-box'
const typeLabel = document.createElement('label')
typeLabel.textContent = 'Config Device Type'
typeLabel.style.display = 'block'
typeLabel.style.marginBottom = '6px'
typeLabel.style.fontWeight = '500'
typeLabel.style.fontSize = '12px'
typeLabel.style.color = '#6b7280'
const typeSelect = document.createElement('select')
typeSelect.style.width = '100%'
typeSelect.style.padding = '8px 10px'
typeSelect.style.marginBottom = '12px'
typeSelect.style.borderRadius = '6px'
typeSelect.style.fontSize = '14px'
typeSelect.style.background = getComputedStyle(document.body).backgroundColor
typeSelect.style.color = getComputedStyle(document.body).color
typeSelect.style.border = '1px solid #ccc'
typeSelect.style.boxSizing = 'border-box'
// Add option groups with all API device types
Object.keys(DEVICE_TYPES).forEach((categoryName) => {
const optgroup = document.createElement('optgroup')
optgroup.label = categoryName
DEVICE_TYPES[categoryName as keyof typeof DEVICE_TYPES].forEach((deviceType) => {
const opt = document.createElement('option')
opt.value = deviceType
opt.text = deviceType
// Select current configDeviceType if set, otherwise match API deviceType
const currentType = device.configDeviceType || device.deviceType || device.type || ''
opt.selected = currentType === deviceType
optgroup.appendChild(opt)
})
typeSelect.appendChild(optgroup)
})
// Create modal dialog for editing
const div = document.createElement('div')
div.style.position = 'fixed'
div.style.top = '0'
div.style.left = '0'
div.style.width = '100%'
div.style.height = '100%'
div.style.background = 'rgba(0,0,0,0.7)'
div.style.display = 'flex'
div.style.alignItems = 'center'
div.style.justifyContent = 'center'
div.style.zIndex = '9999'
const modal = document.createElement('div')
modal.style.background = getComputedStyle(document.body).backgroundColor
modal.style.color = getComputedStyle(document.body).color
modal.style.padding = '0'
modal.style.borderRadius = '10px'
modal.style.minWidth = '440px'
modal.style.maxWidth = '90vw'
modal.style.boxShadow = '0 8px 32px rgba(0,0,0,0.35)'
modal.style.overflow = 'hidden'
modal.style.borderTop = '3px solid var(--switchbot-red, #ef4444)'
const title = document.createElement('h3')
title.textContent = 'Edit Device'
title.style.marginTop = '0'
title.style.marginBottom = '16px'
title.style.padding = '20px 20px 0'
title.style.fontSize = '18px'
title.style.fontWeight = '600'
title.style.color = 'var(--switchbot-red, #ef4444)'
title.style.letterSpacing = '-0.02em'
const contentDiv = document.createElement('div')
contentDiv.style.padding = '0 20px 20px'
const nameLabel = document.createElement('label')
nameLabel.textContent = 'Device Name'
nameLabel.style.display = 'block'
nameLabel.style.marginBottom = '6px'
nameLabel.style.fontWeight = '500'
nameLabel.style.fontSize = '12px'
nameLabel.style.color = '#6b7280'
const nameInput = document.createElement('input')
nameInput.type = 'text'
nameInput.value = device.name || device.id
nameInput.style.width = '100%'
nameInput.style.marginBottom = '12px'
nameInput.style.padding = '8px 10px'
nameInput.style.borderRadius = '6px'
nameInput.style.fontSize = '14px'
nameInput.style.boxSizing = 'border-box'
nameInput.style.transition = 'border-color 0.2s ease'
// Read-only API device type field
const apiTypeLabel = document.createElement('label')
apiTypeLabel.textContent = 'Device Type (API - Read Only)'
apiTypeLabel.style.display = 'block'
apiTypeLabel.style.marginBottom = '6px'
apiTypeLabel.style.fontWeight = '500'
apiTypeLabel.style.fontSize = '12px'
apiTypeLabel.style.color = '#6b7280'
const apiTypeInput = document.createElement('input')
apiTypeInput.type = 'text'
apiTypeInput.value = device.deviceType || device.type || 'Unknown'
apiTypeInput.readOnly = true
apiTypeInput.style.width = '100%'
apiTypeInput.style.marginBottom = '12px'
apiTypeInput.style.padding = '8px 10px'
apiTypeInput.style.borderRadius = '6px'
apiTypeInput.style.fontSize = '13px'
apiTypeInput.style.opacity = '0.6'
apiTypeInput.style.cursor = 'not-allowed'
apiTypeInput.style.boxSizing = 'border-box'
apiTypeInput.style.backgroundColor = '#f9fafb'
// Editable config device type dropdown
// Add option groups with all API device types
Object.keys(DEVICE_TYPES).forEach((categoryName) => {
const optgroup = document.createElement('optgroup')
optgroup.label = categoryName
DEVICE_TYPES[categoryName as keyof typeof DEVICE_TYPES].forEach((deviceType) => {
const opt = document.createElement('option')
opt.value = deviceType
opt.text = deviceType
// Select current configDeviceType if set, otherwise match API deviceType
const currentType = device.configDeviceType || device.deviceType || device.type || ''
opt.selected = currentType === deviceType
optgroup.appendChild(opt)
})
typeSelect.appendChild(optgroup)
})
// --- Connection Preference ---
const connectionPrefLabel = document.createElement('label')
connectionPrefLabel.textContent = 'Connection Preference'
connectionPrefLabel.style.display = 'block'
connectionPrefLabel.style.marginBottom = '6px'
connectionPrefLabel.style.fontWeight = '500'
connectionPrefLabel.style.fontSize = '12px'
connectionPrefLabel.style.color = '#6b7280'
const connectionPrefSelect = document.createElement('select')
connectionPrefSelect.style.width = '100%'
connectionPrefSelect.style.marginBottom = '12px'
connectionPrefSelect.style.padding = '8px 10px'
connectionPrefSelect.style.borderRadius = '6px'
connectionPrefSelect.style.fontSize = '14px'
connectionPrefSelect.style.boxSizing = 'border-box'
;['auto', 'ble', 'openapi'].forEach((val) => {
const opt = document.createElement('option')
opt.value = val
opt.text = val.charAt(0).toUpperCase() + val.slice(1)
opt.selected = (device.connectionPreference || 'auto') === val
connectionPrefSelect.appendChild(opt)
})
// --- Room ---
const roomLabel = document.createElement('label')
roomLabel.textContent = 'Room'
roomLabel.style.display = 'block'
roomLabel.style.marginBottom = '6px'
roomLabel.style.fontWeight = '500'
roomLabel.style.fontSize = '12px'
roomLabel.style.color = '#6b7280'
const roomInput = document.createElement('input')
roomInput.type = 'text'
roomInput.value = device.room || ''
roomInput.placeholder = 'Optional room/location metadata'
roomInput.style.width = '100%'
roomInput.style.marginBottom = '12px'
roomInput.style.padding = '8px 10px'
roomInput.style.borderRadius = '6px'
roomInput.style.fontSize = '14px'
roomInput.style.boxSizing = 'border-box'
// --- Encryption Key ---
const encryptionKeyLabel = document.createElement('label')
encryptionKeyLabel.textContent = 'BLE Encryption Key (optional)'
encryptionKeyLabel.style.display = 'block'
encryptionKeyLabel.style.marginBottom = '6px'
encryptionKeyLabel.style.fontWeight = '500'
encryptionKeyLabel.style.fontSize = '12px'
encryptionKeyLabel.style.color = '#6b7280'
const encryptionKeyInput = document.createElement('input')
encryptionKeyInput.type = 'password'
encryptionKeyInput.value = device.encryptionKey || ''
encryptionKeyInput.placeholder = 'Paste device BLE encryption key'
encryptionKeyInput.style.width = '100%'
encryptionKeyInput.style.marginBottom = '12px'
encryptionKeyInput.style.padding = '8px 10px'
encryptionKeyInput.style.borderRadius = '6px'
encryptionKeyInput.style.fontSize = '14px'
encryptionKeyInput.style.boxSizing = 'border-box'
// --- Key ID ---
const keyIdLabel = document.createElement('label')
keyIdLabel.textContent = 'BLE Key ID (optional)'
keyIdLabel.style.display = 'block'
keyIdLabel.style.marginBottom = '6px'
keyIdLabel.style.fontWeight = '500'
keyIdLabel.style.fontSize = '12px'
keyIdLabel.style.color = '#6b7280'
const keyIdInput = document.createElement('input')
keyIdInput.type = 'text'
keyIdInput.value = device.keyId || ''
keyIdInput.placeholder = 'e.g. ff'
keyIdInput.style.width = '100%'
keyIdInput.style.marginBottom = '12px'
keyIdInput.style.padding = '8px 10px'
keyIdInput.style.borderRadius = '6px'
keyIdInput.style.fontSize = '14px'
keyIdInput.style.boxSizing = 'border-box'
const errorMessage = document.createElement('div')
errorMessage.style.color = 'var(--switchbot-red, #ef4444)'
errorMessage.style.marginBottom = '12px'
errorMessage.style.fontSize = '12px'
errorMessage.style.display = 'none'
errorMessage.style.padding = '8px 10px'
errorMessage.style.background = 'var(--switchbot-red-light, #fee2e2)'
errorMessage.style.borderRadius = '6px'
errorMessage.style.fontWeight = '500'
const buttons = document.createElement('div')
buttons.style.display = 'flex'
buttons.style.gap = '10px'
buttons.style.justifyContent = 'flex-end'
buttons.style.marginTop = '18px'
buttons.style.paddingTop = '18px'
buttons.style.borderTop = '1px solid rgba(0, 0, 0, 0.08)'
const cancelBtn = document.createElement('button')
cancelBtn.textContent = 'Cancel'
cancelBtn.className = 'secondary'
cancelBtn.style.background = '#6b7280'
cancelBtn.style.padding = '8px 16px'
cancelBtn.style.fontSize = '13px'
cancelBtn.onclick = () => div.remove()
const saveBtn = document.createElement('button')
saveBtn.textContent = 'Save'
saveBtn.style.background = 'var(--switchbot-red, #ef4444)'
saveBtn.style.padding = '8px 20px'
saveBtn.style.fontSize = '13px'
saveBtn.onclick = async () => {
try {
const { updateDevice, syncParentPluginConfigFromDisk, fetchDevices } = await import('./api.js')
const { renderDeviceList } = await import('./render.js')
const params = {
deviceId: device.id,
configDeviceName: nameInput.value || undefined,
configDeviceType: typeSelect.value,
connectionPreference: connectionPrefSelect.value,
room: roomInput.value || undefined,
encryptionKey: encryptionKeyInput.value || undefined,
keyId: keyIdInput.value || undefined,
refreshRate: Number(openApiRefreshInput.value) || 300,
blePollingEnabled: blePollingEnabledInput.checked,
blePollIntervalMs: Number(blePollIntervalInput.value) || 600000,
}
// Only include defined properties in the options object
const options: any = {}
if (params.connectionPreference !== undefined) {
options.connectionPreference = params.connectionPreference
}
if (params.room !== undefined) {
options.room = params.room
}
if (params.encryptionKey !== undefined) {
options.encryptionKey = params.encryptionKey
}
if (params.keyId !== undefined) {
options.keyId = params.keyId
}
if (params.refreshRate !== undefined) {
options.refreshRate = params.refreshRate
}
if (params.blePollingEnabled !== undefined) {
options.blePollingEnabled = params.blePollingEnabled
}
if (params.blePollIntervalMs !== undefined) {
options.blePollIntervalMs = params.blePollIntervalMs
}
await updateDevice(
params.deviceId,
params.configDeviceName,
params.configDeviceType,
options,
)
await syncParentPluginConfigFromDisk()
contentDiv.appendChild(openApiRefreshLabel)
contentDiv.appendChild(openApiRefreshInput)
// Refresh device list
uiLog.info('[Edit Device] Refreshing device list after update')
const list = await fetchDevices()
renderDeviceList(list)
div.remove()
} catch (e) {
uiLog.error('Update error:', e)
errorMessage.textContent = `Error: ${e instanceof Error ? e.message : 'Failed to update device'}`
errorMessage.style.display = 'block'
}
}
buttons.appendChild(cancelBtn)
buttons.appendChild(saveBtn)
contentDiv.appendChild(nameLabel)
contentDiv.appendChild(nameInput)
contentDiv.appendChild(apiTypeLabel)
contentDiv.appendChild(apiTypeInput)
contentDiv.appendChild(typeLabel)
contentDiv.appendChild(typeSelect)
contentDiv.appendChild(connectionPrefLabel)
contentDiv.appendChild(connectionPrefSelect)
contentDiv.appendChild(roomLabel)
contentDiv.appendChild(roomInput)
contentDiv.appendChild(encryptionKeyLabel)
contentDiv.appendChild(encryptionKeyInput)
contentDiv.appendChild(keyIdLabel)
contentDiv.appendChild(keyIdInput)
// Insert BLE polling fields before error/buttons
contentDiv.appendChild(openApiRefreshLabel)
contentDiv.appendChild(openApiRefreshInput)
contentDiv.appendChild(blePollingEnabledLabel)
contentDiv.appendChild(blePollingEnabledInput)
contentDiv.appendChild(blePollIntervalLabel)
contentDiv.appendChild(blePollIntervalInput)
contentDiv.appendChild(errorMessage)
contentDiv.appendChild(buttons)
modal.appendChild(title)
modal.appendChild(contentDiv)
div.appendChild(modal)
document.body.appendChild(div)
nameInput.focus()
}