UNPKG

@switchbot/homebridge-switchbot

Version:

The SwitchBot plugin allows you to access your SwitchBot device(s) from HomeKit.

371 lines (344 loc) • 13 kB
// Fetch the list of configured devices from the Homebridge UI API import { isValidDeviceType, normalizeDeviceType } from '../../../device-types.js' import './types.js' import { uiLog } from './logger.js' import { toastError } from './toast.js' export async function fetchDevices(): Promise<any[]> { try { if (typeof homebridge.getPluginConfig !== 'function') { throw new TypeError('Homebridge UI API not available') } const configArr = await homebridge.getPluginConfig() const config = Array.isArray(configArr) && configArr.length > 0 ? configArr.find(isSwitchBotPlatformConfig) : null if (!config || !Array.isArray(config.devices)) { return [] } return config.devices } catch (e) { const msg = e instanceof Error ? e.message : String(e) uiLog.error('Error fetching devices:', msg) return [] } } /** * Validate and auto-correct device types in the config array before saving. * Returns an array of errors for devices that cannot be fixed. */ export function validateAndFixDeviceTypes(devices: Array<{ deviceId: string, configDeviceName: string, configDeviceType: string }>) { const errors: Array<{ deviceId: string, name: string, type: string }> = [] for (const d of devices) { if (!isValidDeviceType(d.configDeviceType)) { const fixed = normalizeDeviceType(d.configDeviceType) if (fixed) { d.configDeviceType = fixed } else { errors.push({ deviceId: d.deviceId, name: d.configDeviceName, type: d.configDeviceType, }) } } } return errors } // API wrapper functions for communicating with the Homebridge UI server function isSwitchBotPlatformConfig(block: any): boolean { const platformName = String(block?.platform || block?.name || '').toLowerCase() return ( platformName === 'switchbot' || platformName === '@switchbot/homebridge-switchbot' || platformName.includes('switchbot') ) } export async function syncParentPluginConfigFromDisk(autoSave = false): Promise<boolean> { try { if ( typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function' ) { uiLog.warn('Parent config sync API not available') return false } // Use Homebridge UI API to fetch and update config const pluginConfigBlocks = await homebridge.getPluginConfig() if (!Array.isArray(pluginConfigBlocks) || !pluginConfigBlocks.length) { uiLog.warn('No plugin config blocks returned from Homebridge') return false } const index = pluginConfigBlocks.findIndex(block => isSwitchBotPlatformConfig(block)) if (index < 0) { uiLog.warn('SwitchBot platform block not found in Homebridge plugin config') return false } // Validate and fix device types before saving const errors = validateAndFixDeviceTypes(pluginConfigBlocks[index].devices || []) if (errors.length > 0) { toastError(`Invalid device types found: ${errors.map(e => `${e.name} (${e.type})`).join(', ')}`) return false } // pluginConfigBlocks[index] is already up to date await homebridge.updatePluginConfig(pluginConfigBlocks) // Auto-save to disk if requested - prevents parent UI from overwriting with stale cache if (autoSave && typeof homebridge.savePluginConfig === 'function') { uiLog.info('Auto-saving config to disk...') await homebridge.savePluginConfig() uiLog.info('Config saved successfully') } return true } catch (e) { uiLog.warn('Failed to sync parent plugin config cache:', e) return false } } export async function fetchCredentialStatus(): Promise<any> { try { const resp = await homebridge.request('/credentials', {}) uiLog.info('Load credentials response:', resp) if (!resp || resp.success === false) { uiLog.error('Failed to load credentials:', resp) return null } return resp.data || {} } catch (e) { uiLog.error('Error loading credentials:', e) return null } } export async function saveCredentials(token: string, secret: string): Promise<any> { uiLog.info('Saving credentials...') const resp = await homebridge.request('/credentials', { token, secret }) uiLog.info('Save response:', resp) if (!resp || resp.success === false) { throw new Error(resp?.message || 'Save failed') } return resp.data || resp } export interface DiscoverRequestOptions { bleEnabled?: boolean bleScanDurationSeconds?: number bleTimeoutSeconds?: number } export async function discoverDevices( mode: 'all' | 'ble' | 'openapi' = 'all', options?: DiscoverRequestOptions, ): Promise<any[]> { const resp = await homebridge.request('/discover', { mode, ...options }) uiLog.info('Discover response:', resp) if (!resp || resp.success === false) { throw new Error(resp?.data?.message || 'Discovery failed') } return resp.data || [] } export async function fetchBluetoothStatus(): Promise<{ available: boolean, message: string }> { try { const resp = await homebridge.request('/ble-status', {}) if (!resp || resp.success === false) { return { available: false, message: 'Bluetooth status unavailable' } } return resp.data || { available: false, message: 'Bluetooth status unavailable' } } catch (_e) { return { available: false, message: 'Bluetooth status unavailable' } } } export async function testDeviceConnection(payload: { deviceId: string connectionType?: string address?: string }): Promise<{ success: boolean deviceId: string method: string latencyMs: number message: string state?: Record<string, any> }> { const resp = await homebridge.request('/test-connection', payload) if (!resp || resp.success === false) { throw new Error(resp?.data?.message || 'Connection test failed') } return resp.data || { success: false, deviceId: payload.deviceId, method: 'Auto', latencyMs: 0, message: 'Connection test failed', } } export async function addDevice( deviceId: string, name: string, type: string, options?: { address?: string, model?: string, rssi?: number, encryptionKey?: string, keyId?: string }, ): Promise<any> { if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') { throw new TypeError('Homebridge UI API not available') } const configArr = await homebridge.getPluginConfig() const idx = Array.isArray(configArr) ? configArr.findIndex(isSwitchBotPlatformConfig) : -1 if (idx === -1) { throw new Error('SwitchBot config not found') } const config = configArr[idx] if (!Array.isArray(config.devices)) { config.devices = [] } const normalizedDeviceId = String(deviceId).trim().toLowerCase() const exists = config.devices.some((d: any) => String(d.deviceId ?? d.id ?? '').trim().toLowerCase() === normalizedDeviceId) if (exists) { return { alreadyExists: true, message: 'Device already in config' } } const newDevice: any = { deviceId, configDeviceName: name, configDeviceType: type } if (options?.address) { newDevice.address = options.address } if (options?.model) { newDevice.model = options.model } if (options?.rssi !== undefined && options?.rssi !== null && options?.rssi !== 0) { newDevice.rssi = options.rssi } if (options?.encryptionKey) { newDevice.encryptionKey = options.encryptionKey } if (options?.keyId) { newDevice.keyId = options.keyId } config.devices.push(newDevice) await homebridge.updatePluginConfig(configArr) if (typeof homebridge.savePluginConfig === 'function') { await homebridge.savePluginConfig() } return { added: true, message: `Device "${name}" added successfully` } } export async function addDevicesInBulk( devices: Array<{ deviceId: string, name: string, type: string, rssi?: number, address?: string, model?: string }>, ): Promise<any> { const resp = await homebridge.request('/add-devices', { devices }) uiLog.info('Bulk add response:', resp) if (!resp || resp.success === false) { throw new Error(resp?.data?.message || 'Bulk add failed') } return normalizeBulkAddDevicesResponse(resp) } export function normalizeBulkAddDevicesResponse(resp: any): any { const payload = resp?.data && typeof resp.data === 'object' ? resp.data : resp const addedCount = Number(payload?.addedCount ?? payload?.added ?? 0) const skippedCount = Number(payload?.skippedCount ?? payload?.skipped ?? 0) const updatedCount = Number(payload?.updatedCount ?? payload?.updated ?? 0) return { ...payload, success: resp?.success ?? true, addedCount, skippedCount, updatedCount, } } export async function updateDevice( deviceId: string, configDeviceName?: string, configDeviceType?: string, options?: { refreshRate?: number connectionPreference?: string encryptionKey?: string keyId?: string room?: string [key: string]: any }, ): Promise<any> { if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') { throw new TypeError('Homebridge UI API not available') } const configArr = await homebridge.getPluginConfig() const idx = Array.isArray(configArr) ? configArr.findIndex(isSwitchBotPlatformConfig) : -1 if (idx === -1) { throw new Error('SwitchBot config not found') } const config = configArr[idx] if (!Array.isArray(config.devices)) { throw new TypeError('No devices array in config') } const normalizedDeviceId = String(deviceId).trim().toLowerCase() const device = config.devices.find((d: any) => String(d.deviceId ?? d.id ?? '').trim().toLowerCase() === normalizedDeviceId) if (!device) { throw new Error('Device not found in config') } if (configDeviceName) { device.configDeviceName = configDeviceName } if (configDeviceType) { device.configDeviceType = configDeviceType } if (options) { Object.assign(device, options) } await homebridge.updatePluginConfig(configArr) if (typeof homebridge.savePluginConfig === 'function') { await homebridge.savePluginConfig() } return { updated: true, message: `Device updated successfully` } } export async function deleteDevice(deviceId: string): Promise<any> { if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') { throw new TypeError('Homebridge UI API not available') } const configArr = await homebridge.getPluginConfig() const idx = Array.isArray(configArr) ? configArr.findIndex(isSwitchBotPlatformConfig) : -1 if (idx === -1) { throw new Error('SwitchBot config not found') } const config = configArr[idx] if (!Array.isArray(config.devices)) { throw new TypeError('No devices array in config') } const normalizedDeviceId = String(deviceId).trim().toLowerCase() const before = config.devices.length // Remove the target device config.devices = config.devices.filter(d => String(d.deviceId ?? d.id ?? '').trim().toLowerCase() !== normalizedDeviceId) // Defensive: filter out any invalid device entries (missing required fields) config.devices = config.devices.filter(d => d && typeof d === 'object' && d.deviceId && d.configDeviceType) if (config.devices.length === before) { throw new Error('Device not found in config') } await homebridge.updatePluginConfig(configArr) if (typeof homebridge.savePluginConfig === 'function') { await homebridge.savePluginConfig() } return { deleted: true, message: `Device removed from config` } } export async function deleteAllDevices(): Promise<any> { if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') { throw new TypeError('Homebridge UI API not available') } const configArr = await homebridge.getPluginConfig() // Find or create the SwitchBot config block let idx = Array.isArray(configArr) ? configArr.findIndex(isSwitchBotPlatformConfig) : -1 if (idx === -1) { // If not found, create a new config block for SwitchBot const newBlock = { platform: 'SwitchBot', devices: [] } configArr.push(newBlock) idx = configArr.length - 1 } const config = configArr[idx] // Always ensure devices is an array if (!Array.isArray(config.devices)) { config.devices = [] } const deletedCount = config.devices.length config.devices = [] // Defensive: ensure required fields for schema compliance if (!config.platform) { config.platform = 'SwitchBot' } // Ensure 'name' property is present (required by schema for Homebridge platform blocks) if (!config.name) { config.name = 'SwitchBot' } // Save updated config await homebridge.updatePluginConfig(configArr) if (typeof homebridge.savePluginConfig === 'function') { await homebridge.savePluginConfig() } return { deleted: true, deletedCount, message: `Removed ${deletedCount} device(s) from config` } }