@switchbot/homebridge-switchbot
Version:
The SwitchBot plugin allows you to access your SwitchBot device(s) from HomeKit.
219 lines • 12.3 kB
JavaScript
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