UNPKG

@switchbot/homebridge-switchbot

Version:

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

219 lines • 12.3 kB
import { RequestError } from '@homebridge/plugin-ui-utils'; import { getCredential, getSwitchBotPlatformConfig } from '../utils/config-parser.js'; import { uiLog } from '../utils/logger.js'; /** * Register discovery endpoint * Discovers SwitchBot devices via BLE and OpenAPI */ export function registerDiscoveryEndpoint(server) { server.onRequest('/ble-status', async () => { try { const { platform } = await getSwitchBotPlatformConfig(server); const token = getCredential(platform, 'openApiToken') || platform.token; const secret = getCredential(platform, 'openApiSecret') || platform.secret; const { SwitchBot } = await import('node-switchbot'); const switchbot = new SwitchBot({ token: token || undefined, secret: secret || undefined, enableBLE: true, enableFallback: true, enableRetry: true, enableCircuitBreaker: true, enableConnectionIntelligence: true, }); await switchbot.discover({ timeout: 1000 }); return { success: true, data: { available: true, message: 'adapter ready' } }; } catch (e) { const message = e instanceof Error ? e.message : String(e); uiLog.warn(`GET /ble-status failed: ${message}`); return { success: true, data: { available: false, message } }; } }); server.onRequest('/discover', async (payload) => { uiLog.debug(`[SwitchBot UI/Server] /discover incoming payload: ${JSON.stringify(payload)}`); try { const { platform } = await getSwitchBotPlatformConfig(server); const mode = String(payload?.mode || 'all').toLowerCase(); // Only run BLE if mode is all/ble AND bleEnabled is not false (default true if missing) const bleEnabled = payload?.bleEnabled !== false; const runBle = (mode === 'all' || mode === 'ble') && bleEnabled; const runOpenApi = mode === 'all' || mode === 'openapi'; const bleScanDurationSeconds = Math.max(3, Math.min(15, Number(payload?.bleScanDurationSeconds || 5))); const bleTimeoutSeconds = Math.max(3, Math.min(30, Number(payload?.bleTimeoutSeconds || 8))); uiLog.debug(`GET /discover - Platform config keys: ${Object.keys(platform).join(', ')}`); const token = getCredential(platform, 'openApiToken') || platform.token; const secret = getCredential(platform, 'openApiSecret') || platform.secret; const hasOpenAPICredentials = !!(token && secret); if (!hasOpenAPICredentials) { uiLog.warn('GET /discover - No OpenAPI credentials found, will attempt BLE-only discovery'); } else { uiLog.info('GET /discover - Using OpenAPI credentials for discovery'); } // Import and initialize node-switchbot const { SwitchBot } = await import('node-switchbot'); const switchbot = new SwitchBot({ token: token || undefined, secret: secret || undefined, enableBLE: true, enableFallback: true, enableRetry: true, enableCircuitBreaker: true, enableConnectionIntelligence: true, }); const deviceMap = new Map(); // 1. Try BLE discovery first (with timeout) if (runBle) { uiLog.info('GET /discover - Starting BLE scan...'); try { const bleTimeout = bleTimeoutSeconds * 1000; const bleDiscoveryPromise = switchbot.discover({ timeout: bleScanDurationSeconds * 1000 }); const bleDevices = await Promise.race([ bleDiscoveryPromise, new Promise(resolve => setTimeout(resolve, bleTimeout, [])), ]); if (Array.isArray(bleDevices) && bleDevices.length > 0) { uiLog.info(`GET /discover - Found ${bleDevices.length} BLE devices`); for (const [index, d] of bleDevices.entries()) { const info = typeof d?.getInfo === 'function' ? d.getInfo() : undefined; const id = d?.id || (typeof d?.getId === 'function' ? d.getId() : undefined) || info?.id; const mac = d?.mac || (typeof d?.getMAC === 'function' ? d.getMAC() : undefined) || info?.mac; if (!id) { uiLog.warn(`GET /discover - BLE device at index ${index} has no id, skipping`); continue; } const connectionTypes = Array.isArray(info?.connectionTypes) ? info.connectionTypes : []; const isHybrid = connectionTypes.includes('api'); deviceMap.set(id, { id, name: d?.name || (typeof d?.getName === 'function' ? d.getName() : undefined) || info?.name || d?.deviceName || id, type: d?.deviceType || (typeof d?.getDeviceType === 'function' ? d.getDeviceType() : undefined) || info?.deviceType || d?.type || d?.model || 'unknown', model: info?.model || d?.model || d?.deviceModel, address: mac, connectionType: isHybrid ? 'Both' : 'BLE', rssi: info?.rssi || d?.rssi, }); } } else { uiLog.info('GET /discover - No BLE devices found or scan timed out'); } } catch (bleErr) { uiLog.warn(`GET /discover - BLE discovery failed: ${bleErr instanceof Error ? bleErr.message : String(bleErr)}`); // Continue with OpenAPI even if BLE fails } } // 2. Get devices from OpenAPI (only if credentials are available) if (runOpenApi && hasOpenAPICredentials) { uiLog.info('GET /discover - Fetching devices from OpenAPI...'); try { const apiClient = switchbot.getAPIClient(); if (!apiClient) { throw new Error('API client not available - token/secret may be missing'); } const apiData = await apiClient.getDevices(); uiLog.debug(`GET /discover - OpenAPI response: ${JSON.stringify(apiData)}`); // Parse physical devices - apiData is DeviceListResponse with deviceList and infraredRemoteList const devices = apiData.deviceList || []; const irDevices = apiData.infraredRemoteList || []; uiLog.info(`GET /discover - Found ${devices.length} OpenAPI physical devices and ${irDevices.length} IR devices`); // Process physical devices from OpenAPI for (const d of devices) { const id = d.deviceId; if (!id) { continue; } const existing = deviceMap.get(id); if (existing) { // Device found via both BLE and OpenAPI existing.connectionType = 'Both'; existing.name = d.deviceName || existing.name; existing.type = d.deviceType || existing.type; // Note: APIDevice doesn't have a model property existing.enabled = d.enableCloudService !== false; existing.hubDeviceId = d.hubDeviceId; } else { // Device only found via OpenAPI deviceMap.set(id, { id, name: d.deviceName || id, type: d.deviceType || 'unknown', enabled: d.enableCloudService !== false, hubDeviceId: d.hubDeviceId, connectionType: 'OpenAPI', }); } } // Process IR devices (OpenAPI only) for (const d of irDevices) { const id = d.deviceId; if (!id) { continue; } deviceMap.set(id, { id, name: d.deviceName || id, type: d.remoteType || 'unknown', enabled: true, hubDeviceId: d.hubDeviceId, connectionType: 'OpenAPI', isIR: true, }); } } catch (apiErr) { uiLog.error(`GET /discover - OpenAPI discovery failed: ${apiErr instanceof Error ? apiErr.message : String(apiErr)}`); // If we have BLE devices, we can still return those if (deviceMap.size === 0) { throw apiErr; } } } else if (runOpenApi) { uiLog.info('GET /discover - Skipping OpenAPI discovery (no credentials configured)'); } const normalizedBleDevices = [...deviceMap.values()].filter(d => d.connectionType === 'BLE' || d.connectionType === 'Both'); const firstNormalizedBleId = normalizedBleDevices[0]?.id || 'none'; uiLog.info(`GET /discover - Normalized BLE devices: ${normalizedBleDevices.length} (firstId: ${firstNormalizedBleId})`); // Check if we found any devices if (deviceMap.size === 0 && mode === 'openapi' && !hasOpenAPICredentials) { return { success: true, data: [] }; } if (deviceMap.size === 0) { const errorMsg = hasOpenAPICredentials ? 'No devices found via BLE or OpenAPI. Make sure devices are powered on and in range.' : 'No devices found via BLE. OpenAPI credentials not configured. Please save your credentials in settings to discover cloud-connected devices, or ensure BLE devices are powered on and in range.'; uiLog.error(`GET /discover - ${errorMsg}`); throw new Error(errorMsg); } // Convert map to array const allDiscovered = [...deviceMap.values()]; const bleCount = allDiscovered.filter(d => d.connectionType === 'BLE').length; const apiCount = allDiscovered.filter(d => d.connectionType === 'OpenAPI').length; const bothCount = allDiscovered.filter(d => d.connectionType === 'Both').length; uiLog.info(`GET /discover - Total: ${allDiscovered.length} devices (BLE: ${bleCount}, OpenAPI: ${apiCount}, Both: ${bothCount})`); return { success: true, data: allDiscovered }; } catch (e) { if (e instanceof Error) { uiLog.error(`Error in /discover: ${e.message}`); if (e.stack) { uiLog.error(`[Stack] ${e.stack}`); } uiLog.error(`[Object]`, e); } else { uiLog.error(`Error in /discover: ${String(e)}`); } throw new RequestError(`Failed to discover devices: ${e instanceof Error ? e.message : String(e)}`, e); } }); } //# sourceMappingURL=discovery.js.map