homebridge-switchbot-ble
Version:
SwitchBot BLE plugin for Homebridge using direct BLE control. Features device mode verification for ON/OFF commands and improved retry logic. All logs use local time (new Date().toLocaleString()).
578 lines (489 loc) • 21.4 kB
JavaScript
/**
* SwitchBot Bot Command Line Tool
* Directly use switchbot-api-server.mjs module to control SwitchBot Bot devices
*/
import * as SwitchBotAPI from './switchbot-api-server.mjs';
import { createInterface } from 'readline';
// Detect if it's API call mode
const IS_API_MODE = process.env.NODE_ENV === 'api' ||
process.argv.includes('--api-mode') ||
(process.argv[1] && !process.argv[1].endsWith('bot-cmd.mjs'));
// Set auto exit timeout (only for API mode)
if (IS_API_MODE) {
const AUTO_EXIT_TIMEOUT = 15000; // Auto exit after 15 seconds
setTimeout(() => {
console.log('API mode: operation timeout, auto exit');
process.exit(0);
}, AUTO_EXIT_TIMEOUT);
}
// Get command line parameters
const args = process.argv.slice(2);
const command = args[0];
const deviceId = args[1];
// Create readline interface
let readline;
// Create readline only when needed
function getReadline() {
if (!readline) {
readline = createInterface({
input: process.stdin,
output: process.stdout
});
}
return readline;
}
// Wrap readline.question as Promise
function question(query) {
return new Promise(resolve => {
getReadline().question(query, resolve);
});
}
// Close readline interface (only when created)
function closeReadlineIfNeeded() {
if (readline) {
readline.close();
readline = null;
}
}
// Normalize MAC address format, ensure consistency
function normalizeMacAddress(mac) {
if (!mac) return null;
// If already standard format (xx:xx:xx:xx:xx:xx), return lowercase
if (/^([0-9a-f]{2}:){5}[0-9a-f]{2}$/i.test(mac)) {
return mac.toLowerCase();
}
// Remove all non-hex characters
const cleanMac = mac.replace(/[^0-9a-f]/gi, '');
// If 12 characters left, format as standard MAC
if (cleanMac.length === 12) {
return cleanMac.match(/.{1,2}/g).join(':').toLowerCase();
}
// If not standard, return original
return mac.toLowerCase();
}
// Compare two MAC addresses (considering different formats)
function compareMacAddresses(mac1, mac2) {
const normalized1 = normalizeMacAddress(mac1);
const normalized2 = normalizeMacAddress(mac2);
if (!normalized1 || !normalized2) return false;
return normalized1 === normalized2;
}
// Helper function: Format device status output
function formatDeviceStatus(status) {
if (!status) return 'Unable to get status';
if (status.error) return `Error: ${status.error}`;
return `
Device ID: ${status.deviceId}
Type: ${status.type}
Mode: ${status.mode || 'Unknown'}
State: ${status.state === 'ON' ? 'On' : status.state === 'OFF' ? 'Off' : 'Unknown'}
Battery: ${status.battery !== null ? `${status.battery}%` : 'Unknown'}
`;
}
// Show usage instructions
function showHelp() {
console.log(`
SwitchBot Bot Command Line Tool
Usage:
node bot-cmd.mjs <command> [args]
Available commands:
scan Scan for nearby SwitchBot devices
status [deviceID] Get device status; if not provided, will scan first
press [deviceID] Press Bot; if not provided, will scan first
on [deviceID] Turn on Bot (switch mode only); if not provided, will scan first
off [deviceID] Turn off Bot (switch mode only); if not provided, will scan first
auto-on Automatically turn on all scanned Bot devices
auto-off Automatically turn off all scanned Bot devices
server View server status
normalize <MAC> Normalize MAC address format
find <MAC> Find device by MAC address (supports non-standard format)
Examples:
node bot-cmd.mjs scan
node bot-cmd.mjs status # Scan and select device to view status
node bot-cmd.mjs press aa:bb:cc:dd:ee:ff
node bot-cmd.mjs on # Scan and select device to turn on
node bot-cmd.mjs off aa:bb:cc:dd:ee:ff
node bot-cmd.mjs auto-on # Automatically turn on all scanned Bot devices
node bot-cmd.mjs find DD0B7215C339 # Find device using any format
`);
}
// Scan devices and let user select
async function scanAndSelect() {
console.log('Scanning for nearby SwitchBot devices... (max 3 seconds)');
const devices = await SwitchBotAPI.scanDevices({ duration: 3000 });
if (devices.length === 0) {
console.log('No SwitchBot devices found');
return null;
}
console.log(`Found ${devices.length} SwitchBot devices:`);
devices.forEach((device, index) => {
console.log(`\n[${index + 1}] Device:`);
console.log(` Device ID: ${device.address}`);
console.log(` Type: ${device.type || 'Bot'}`);
console.log(` Mode: ${device.mode || 'Unknown'}`);
console.log(` State: ${device.state || 'Unknown'}`);
console.log(` Battery: ${device.battery !== undefined ? `${device.battery}%` : 'Unknown'}`);
});
// Let user select device
const answer = await question('\nPlease select the device to operate (enter number) or enter q to exit: ');
if (answer.toLowerCase() === 'q') {
return null;
}
const index = parseInt(answer, 10) - 1;
if (isNaN(index) || index < 0 || index >= devices.length) {
console.log('Invalid selection');
return null;
}
return devices[index].address;
}
// Find device by MAC address, supports non-standard format
async function findDeviceByMac(targetMac) {
if (!targetMac) {
console.log('Please provide a MAC address');
return null;
}
const normalizedMac = normalizeMacAddress(targetMac);
console.log(`Trying to find device: ${targetMac} (normalized: ${normalizedMac})`);
console.log('Scanning... (max 3 seconds, will finish early if device found)');
// Use targeted scan to find device faster
const devices = await SwitchBotAPI.scanDevices({
duration: 3000,
targetAddress: normalizedMac
});
if (devices.length === 0) {
console.log('No SwitchBot devices found');
// If not found but MAC provided, still return provided MAC for operation
console.log(`No device found, will use provided MAC address: ${normalizedMac}`);
return normalizedMac;
}
// Try exact match
const matchedDevice = devices.find(d =>
compareMacAddresses(d.address, targetMac) ||
compareMacAddresses(d.id, targetMac)
);
if (matchedDevice) {
console.log(`Found exact match: ${matchedDevice.address}`);
return matchedDevice.address;
}
// Try partial match (if no exact match)
const partialMatches = devices.filter(d => {
const cleanTarget = targetMac.replace(/[^0-9a-f]/gi, '').toLowerCase();
const cleanAddr = d.address.replace(/[^0-9a-f]/gi, '').toLowerCase();
const cleanId = (d.id || '').replace(/[^0-9a-f]/gi, '').toLowerCase();
return cleanAddr.includes(cleanTarget) ||
cleanTarget.includes(cleanAddr) ||
cleanId.includes(cleanTarget) ||
cleanTarget.includes(cleanId);
});
if (partialMatches.length === 1) {
console.log(`Found partial match: ${partialMatches[0].address}`);
return partialMatches[0].address;
} else if (partialMatches.length > 1) {
console.log(`Found ${partialMatches.length} possible matches:`);
// Check if API mode or just use provided MAC
if (IS_API_MODE) {
// In API mode, auto-select first match
console.log(`API mode: auto-selecting first device ${partialMatches[0].address}`);
return partialMatches[0].address;
}
// Even if multiple matches, still use provided MAC
console.log(`Multiple matches found, will use provided MAC address: ${normalizedMac}`);
return normalizedMac;
}
// If no match, still return provided MAC
console.log(`No match found, will use provided MAC address: ${normalizedMac}`);
return normalizedMac;
}
// Automatically operate all scanned Bot devices
async function autoOperateAllBots(operation) {
console.log(`Scanning for nearby SwitchBot devices to ${operation === 'on' ? 'turn on' : 'turn off'}... (max 3 seconds)`);
const devices = await SwitchBotAPI.scanDevices({ duration: 3000 });
if (devices.length === 0) {
console.log('No SwitchBot devices found');
return;
}
console.log(`Found ${devices.length} devices, preparing to ${operation === 'on' ? 'turn on' : 'turn off'}:`);
// Ask user to confirm operating all devices
const confirmation = await question(`Are you sure you want to ${operation === 'on' ? 'turn on' : 'turn off'} all these devices? (y/n): `);
if (confirmation.toLowerCase() !== 'y') {
console.log('Operation cancelled');
return;
}
// Record successful and failed operations
const results = {
success: [],
failed: [],
skipped: [] // For devices not in Switch mode
};
// Perform operation for each device
for (let i = 0; i < devices.length; i++) {
const device = devices[i];
console.log(`\n[${i + 1}/${devices.length}] Checking device: ${device.address}`);
// Check device mode first
try {
console.log('Checking device mode... (will finish early if device found)');
const status = await SwitchBotAPI.getBotStatus(device.address);
// Skip devices not in Switch mode
if (status && status.mode !== 'Switch') {
console.log(`⚠ Skipping device ${device.address}: Device is in ${status.mode || 'unknown'} mode, not Switch mode`);
results.skipped.push({
address: device.address,
reason: `Device is in ${status.mode || 'unknown'} mode, not Switch mode`
});
continue;
}
console.log(`[${i + 1}/${devices.length}] ${operation === 'on' ? 'Turning on' : 'Turning off'} device: ${device.address}`);
const result = operation === 'on'
? await SwitchBotAPI.turnOnBot(device.address)
: await SwitchBotAPI.turnOffBot(device.address);
if (result.commandSent) {
console.log(`✓ ${operation === 'on' ? 'Turn on' : 'Turn off'} command sent successfully`);
results.success.push(device.address);
} else {
console.log(`✗ ${operation === 'on' ? 'Turn on' : 'Turn off'} command send failed: ${result.error || 'Unknown error'}`);
results.failed.push({
address: device.address,
error: result.error || 'Unknown error'
});
}
// Pause between operations to avoid Bluetooth stack overload
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
console.error(`✗ Operation failed: ${error.message}`);
results.failed.push({
address: device.address,
error: error.message
});
}
}
// Display operation result summary
console.log('\nOperation summary:');
console.log(`✓ Success: ${results.success.length} devices`);
console.log(`✗ Failed: ${results.failed.length} devices`);
console.log(`⚠ Skipped: ${results.skipped.length} devices (not in Switch mode)`);
if (results.failed.length > 0) {
console.log('\nFailed devices:');
results.failed.forEach((item, index) => {
console.log(` ${index + 1}. ${item.address} - Error: ${item.error}`);
});
}
if (results.skipped.length > 0) {
console.log('\nSkipped devices (not in Switch mode):');
results.skipped.forEach((item, index) => {
console.log(` ${index + 1}. ${item.address} - Reason: ${item.reason}`);
});
}
}
// Main program
async function main() {
// Check command
if (!command || command === 'help') {
showHelp();
return;
}
try {
// Normalize device ID
const normalizedDeviceId = deviceId ? normalizeMacAddress(deviceId) : null;
if (normalizedDeviceId !== deviceId && deviceId) {
console.log(`Original device ID: ${deviceId}`);
console.log(`Normalized MAC address: ${normalizedDeviceId}`);
}
switch (command.toLowerCase()) {
case 'scan':
console.log('Scanning for nearby SwitchBot devices... (max 3 seconds)');
const devices = await SwitchBotAPI.scanDevices({ duration: 3000 });
if (devices.length === 0) {
console.log('No SwitchBot devices found');
closeReadlineIfNeeded();
process.exit(0);
} else {
console.log(`Found ${devices.length} SwitchBot devices:`);
devices.forEach((device, index) => {
console.log(`\nDevice ${index + 1}:`);
console.log(`Device ID: ${device.address}`);
console.log(`Type: ${device.type || 'Bot'}`);
console.log(`Mode: ${device.mode || 'Unknown'}`);
console.log(`State: ${device.state || 'Unknown'}`);
console.log(`Battery: ${device.battery !== undefined ? `${device.battery}%` : 'Unknown'}`);
});
closeReadlineIfNeeded();
process.exit(0);
}
break;
case 'find':
if (!deviceId) {
console.log('Please provide the MAC address to search');
showHelp();
break;
}
const foundDevice = await findDeviceByMac(deviceId);
if (foundDevice) {
console.log(`Device found! Normalized MAC address: ${foundDevice}`);
const status = await SwitchBotAPI.getBotStatus(foundDevice);
console.log(formatDeviceStatus(status));
setTimeout(() => { closeReadlineIfNeeded(); process.exit(0); }, 100);
} else {
console.log('No matching device found');
closeReadlineIfNeeded();
process.exit(0);
}
break;
case 'normalize':
if (!deviceId) {
console.log('Please provide the MAC address to normalize');
break;
}
const normalizedMac = normalizeMacAddress(deviceId);
console.log(`Original MAC: ${deviceId}`);
console.log(`Normalized MAC: ${normalizedMac}`);
break;
case 'status': {
let targetDeviceId = normalizedDeviceId;
if (deviceId) {
targetDeviceId = await findDeviceByMac(deviceId);
}
// If no device ID provided, scan and select
if (!targetDeviceId) {
targetDeviceId = await scanAndSelect();
if (!targetDeviceId) {
console.log('Operation cancelled');
closeReadlineIfNeeded();
process.exit(0);
break;
}
}
console.log(`Getting device ${targetDeviceId} status...`);
console.log('Scanning... (max 3 seconds, will finish early if device found)');
const status = await SwitchBotAPI.getBotStatus(targetDeviceId);
console.log(formatDeviceStatus(status));
closeReadlineIfNeeded();
process.exit(0);
break;
}
case 'press': {
let targetDeviceId = normalizedDeviceId;
if (deviceId) {
targetDeviceId = await findDeviceByMac(deviceId);
}
// If no device ID provided, scan and select
if (!targetDeviceId) {
targetDeviceId = await scanAndSelect();
if (!targetDeviceId) {
console.log('Operation cancelled');
closeReadlineIfNeeded();
process.exit(0);
break;
}
}
console.log(`Pressing device ${targetDeviceId}...`);
const pressResult = await SwitchBotAPI.pressBot(targetDeviceId);
console.log('✓ Press command sent successfully');
closeReadlineIfNeeded();
process.exit(0);
break;
}
case 'on': {
let targetDeviceId = normalizedDeviceId;
if (deviceId) {
targetDeviceId = await findDeviceByMac(deviceId);
}
// If no device ID provided, scan and select
if (!targetDeviceId) {
targetDeviceId = await scanAndSelect();
if (!targetDeviceId) {
console.log('Operation cancelled');
closeReadlineIfNeeded();
process.exit(0);
break;
}
}
// Check device mode before turning on
console.log(`Checking device ${targetDeviceId} mode...`);
console.log('Scanning... (max 3 seconds, will finish early if device found)');
const status = await SwitchBotAPI.getBotStatus(targetDeviceId);
if (status && status.mode !== 'Switch') {
console.log(`⚠ Cannot turn on device: Device is in ${status.mode || 'unknown'} mode, not Switch mode`);
console.log(`For devices in Press mode, use 'press' command instead of 'on'`);
closeReadlineIfNeeded();
process.exit(1);
break;
}
console.log(`Turning on device ${targetDeviceId}...`);
const onResult = await SwitchBotAPI.turnOnBot(targetDeviceId);
console.log('✓ Turn on command sent successfully');
closeReadlineIfNeeded();
process.exit(0);
break;
}
case 'off': {
let targetDeviceId = normalizedDeviceId;
if (deviceId) {
targetDeviceId = await findDeviceByMac(deviceId);
}
// If no device ID provided, scan and select
if (!targetDeviceId) {
targetDeviceId = await scanAndSelect();
if (!targetDeviceId) {
console.log('Operation cancelled');
closeReadlineIfNeeded();
process.exit(0);
break;
}
}
// Check device mode before turning off
console.log(`Checking device ${targetDeviceId} mode...`);
console.log('Scanning... (max 3 seconds, will finish early if device found)');
const status = await SwitchBotAPI.getBotStatus(targetDeviceId);
if (status && status.mode !== 'Switch') {
console.log(`⚠ Cannot turn off device: Device is in ${status.mode || 'unknown'} mode, not Switch mode`);
console.log(`For devices in Press mode, use 'press' command instead of 'off'`);
closeReadlineIfNeeded();
process.exit(1);
break;
}
console.log(`Turning off device ${targetDeviceId}...`);
const offResult = await SwitchBotAPI.turnOffBot(targetDeviceId);
console.log('✓ Turn off command sent successfully');
closeReadlineIfNeeded();
process.exit(0);
break;
}
case 'auto-on': {
// Automatically turn on all scanned Bot devices
await autoOperateAllBots('on');
break;
}
case 'auto-off': {
// Automatically turn off all scanned Bot devices
await autoOperateAllBots('off');
break;
}
case 'server':
console.log('Getting server status...');
const serverStatus = await SwitchBotAPI.getServerStatus();
console.log(`
Platform: ${serverStatus.platform}
Node version: ${serverStatus.nodeVersion}
Uptime: ${serverStatus.uptime}
Admin rights: ${serverStatus.adminRights ? 'Yes' : 'No'}
${serverStatus.adminMessage}
`);
break;
default:
console.error(`Unknown command: ${command}`);
showHelp();
}
} catch (error) {
console.error('Error executing command:', error.message);
} finally {
// Close readline interface
closeReadlineIfNeeded();
}
}
// Execute main program
main().catch(error => {
console.error('Error executing program:', error);
closeReadlineIfNeeded();
process.exit(1);
});