UNPKG

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()).

649 lines (556 loc) 33.3 kB
const path = require('path'); const { exec } = require('child_process'); const fs = require('fs'); // Create a log directory if it doesn't exist const LOG_DIR = path.join(__dirname, 'logs'); if (!fs.existsSync(LOG_DIR)) { try { fs.mkdirSync(LOG_DIR); } catch (err) { console.error('Failed to create log directory:', err.message); } } // Log helper function function logToFile(message, level = 'INFO') { try { const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format const logFile = path.join(LOG_DIR, `switchbot-api-${today}.log`); const timestamp = new Date().toISOString(); const logPrefix = `[${timestamp}] [${level}]`; const logMessage = `${logPrefix} ${message}\n`; fs.appendFileSync(logFile, logMessage); } catch (err) { console.error('Failed to write to log file:', err.message); } } module.exports = (api) => { // Register only as a dynamic platform plugin api.registerPlatform('homebridge-switchbot-ble', 'SwitchbotBLE', SwitchbotPlatform); }; class SwitchbotPlatform { constructor(log, config, api) { this.log = log; this.config = config || {}; this.api = api; this.accessories = []; this.PLUGIN_NAME = 'homebridge-switchbot-ble'; // Ensure devices are set this.devices = this.config.devices || []; this.log.info('SwitchBot BLE Platform initialized'); logToFile('SwitchBot BLE Platform initialized', 'PLATFORM'); this.log.debug(`Configured ${this.devices.length} SwitchBot devices`); logToFile(`Configured ${this.devices.length} SwitchBot devices`, 'DEBUG'); // When Homebridge is fully launched, discover devices this.api.on('didFinishLaunching', () => { this.log.debug('Finished launching, discovering accessories...'); logToFile('Finished launching, discovering accessories...', 'PLATFORM'); this.discoverDevices(); }); } // Configure accessory restored from cache configureAccessory(accessory) { this.log.debug(`Restored accessory from cache: ${accessory.displayName}`); logToFile(`Restored accessory from cache: ${accessory.displayName}`, 'CACHE'); // Set controller for restored accessory const deviceInfo = accessory.context.device; if (deviceInfo) { this.configureController(accessory); // Add accessory to tracking list this.accessories.push(accessory); } } // Set controller for accessory configureController(accessory) { const deviceInfo = accessory.context.device; if (!deviceInfo) { this.log.warn(`Accessory ${accessory.displayName} has no device info`); return; } // Create and set controller new SwitchbotController(this.log, deviceInfo, this.api, accessory); } // Discover and register devices discoverDevices() { this.log.info('Discovering accessories...'); logToFile('Discovering accessories...', 'PLATFORM'); // Check device list if (this.devices.length === 0) { this.log.warn('No devices configured. Check your config.json'); logToFile('No devices configured. Check your config.json', 'WARNING'); return; } // Track accessories to keep const foundAccessories = []; // For each device, create or update accessory for (const deviceConfig of this.devices) { // Ensure each device has unique name and deviceId if (!deviceConfig.name || !deviceConfig.deviceId) { this.log.warn('Device config missing required fields, skipping:', JSON.stringify(deviceConfig)); logToFile(`Device config missing required fields, skipping: ${JSON.stringify(deviceConfig)}`, 'WARNING'); continue; } // Create unique ID const uuid = this.api.hap.uuid.generate(deviceConfig.deviceId); // Check if accessory already exists let accessory = this.accessories.find(a => a.UUID === uuid); if (accessory) { // Existing accessory, update info this.log.info(`Updating existing device: ${deviceConfig.name} (${deviceConfig.deviceId})`); logToFile(`Updating existing device: ${deviceConfig.name} (${deviceConfig.deviceId})`, 'UPDATE'); // Update accessory context accessory.context.device = deviceConfig; accessory.displayName = deviceConfig.name; // Reconfigure controller this.configureController(accessory); } else { // Create new accessory this.log.info(`Adding new device: ${deviceConfig.name} (${deviceConfig.deviceId})`); logToFile(`Adding new device: ${deviceConfig.name} (${deviceConfig.deviceId})`, 'ADD'); // Create platform accessory accessory = new this.api.platformAccessory(deviceConfig.name, uuid); // Set accessory type and data accessory.category = this.api.hap.Categories.SWITCH; accessory.context.device = deviceConfig; // Configure controller this.configureController(accessory); // Register new accessory this.api.registerPlatformAccessories(this.PLUGIN_NAME, 'SwitchbotBLE', [accessory]); } // Add to found accessories list foundAccessories.push(accessory); } // Handle accessories to remove const accessoriesToRemove = this.accessories.filter(existingAccessory => !foundAccessories.some(a => a.UUID === existingAccessory.UUID) ); if (accessoriesToRemove.length > 0) { this.log.info(`Removing ${accessoriesToRemove.length} unused accessories`); logToFile(`Removing ${accessoriesToRemove.length} unused accessories`, 'REMOVE'); this.api.unregisterPlatformAccessories(this.PLUGIN_NAME, 'SwitchbotBLE', accessoriesToRemove); } // Update tracked accessories list this.accessories = foundAccessories; this.log.info(`Successfully discovered and configured ${this.accessories.length} devices`); logToFile(`Successfully discovered and configured ${this.accessories.length} devices`, 'PLATFORM'); } } // Controller class, used to configure platform accessories class SwitchbotController { constructor(log, config, api, platformAccessory) { this.log = log; this.config = config; this.api = api; this.platformAccessory = platformAccessory; this.deviceId = config.deviceId; this.name = config.name; this.logPrefix = `[${this.name}] `; // Track state - default to OFF to avoid triggering commands on init this.currentState = false; // 添加状态检查相关配置 this.enableStatusCheck = config.enableStatusCheck || false; this.statusCheckInterval = config.statusCheckInterval || 60; // seconds, default 60s this.statusCheckTimer = null; // 用于存储定时器句柄 // Get service and characteristic references this.Service = this.api.hap.Service; this.Characteristic = this.api.hap.Characteristic; // Configure accessory services this.configureServices(); // Try to get device state on initialization this.refreshDeviceState(); this.log.debug(`${this.logPrefix}SwitchbotController initialized`); logToFile(`${this.logPrefix}SwitchbotController initialized`, 'CONTROLLER'); // 如果启用了状态检查,开始定时检查 if (this.enableStatusCheck && this.deviceId) { this.startStatusCheckTimer(); this.log.info(`${this.logPrefix}Automatic status check enabled, interval: ${this.statusCheckInterval}s`); logToFile(`${this.logPrefix}Automatic status check enabled, interval: ${this.statusCheckInterval}s`); } // 当插件停止时清除定时器 this.api.on('shutdown', () => { this.stopStatusCheckTimer(); this.log.info(`${this.logPrefix}Plugin shutting down, stopped status check timer`); logToFile(`${this.logPrefix}Plugin shutting down, stopped status check timer`); }); } configureServices() { // Add switch service this.switchService = this.platformAccessory.getService(this.Service.Switch) || this.platformAccessory.addService(this.Service.Switch, this.name); // Configure switch characteristic this.switchService .getCharacteristic(this.Characteristic.On) .onGet(() => { this.log.debug(`${this.logPrefix}Get switch state: ${this.currentState ? 'ON' : 'OFF'}`); logToFile(`${this.logPrefix}Get switch state: ${this.currentState ? 'ON' : 'OFF'}`, 'STATE'); // Return current tracked state return this.currentState; }) .onSet(async (value) => { this.log.info(`${this.logPrefix}Set switch state to ${value ? 'ON' : 'OFF'}`); logToFile(`${this.logPrefix}Set switch state to ${value ? 'ON' : 'OFF'}`, 'STATE'); // Update tracked state this.currentState = value; // Immediately return new state, do not wait for command to finish // This avoids "This plugin slows down Homebridge" warning // Execute command in background setImmediate(() => { this.executeCommand(value).catch(error => { this.log.error(`${this.logPrefix}Error executing command: ${error.message}`); logToFile(`${this.logPrefix}Error executing command: ${error.message}`, 'ERROR'); }); }); }); // Configure information service this.infoService = this.platformAccessory.getService(this.Service.AccessoryInformation) || this.platformAccessory.addService(this.Service.AccessoryInformation); this.infoService .setCharacteristic(this.Characteristic.Manufacturer, 'SwitchBot') .setCharacteristic(this.Characteristic.Model, 'Bot') .setCharacteristic(this.Characteristic.SerialNumber, this.deviceId || 'Unknown') .setCharacteristic(this.Characteristic.FirmwareRevision, '1.0.0'); } // Execute command async executeCommand(value) { const cmdPath = path.join(__dirname, 'bot-cmd.mjs'); const cmd = value ? 'on' : 'off'; const fullCmd = `node "${cmdPath}" ${cmd} ${this.deviceId}`; this.log.info(`${this.logPrefix}Executing command: ${fullCmd}`); logToFile(`${this.logPrefix}Executing command: ${fullCmd}`, 'COMMAND'); // Set max retry count const maxRetries = 5; let retryCount = 0; let lastError = null; // Refactor to use async/await and Promise for reliability return new Promise(async (resolve) => { // Wrap command execution as Promise const runCommand = () => { return new Promise((cmdResolve) => { const retryPrefix = retryCount > 0 ? `[Retry ${retryCount}/${maxRetries}] ` : ''; if (retryCount > 0) { this.log.info(`${this.logPrefix}${retryPrefix}Executing retry command`); logToFile(`${this.logPrefix}${retryPrefix}Executing retry command`, 'RETRY'); } exec(fullCmd, { timeout: 30000 }, (error, stdout, stderr) => { if (stderr) { this.log.debug(`${this.logPrefix}${retryPrefix}Command stderr: ${stderr}`); logToFile(`${this.logPrefix}${retryPrefix}Command stderr: ${stderr}`, 'ERROR'); } if (stdout) { this.log.debug(`${this.logPrefix}${retryPrefix}Command stdout: ${stdout}`); logToFile(`${this.logPrefix}${retryPrefix}Command stdout: ${stdout}`, 'DEBUG'); } cmdResolve({ error, stdout, stderr }); }); }); }; // Use loop instead of recursion for retries while (retryCount <= maxRetries) { const { error, stdout, stderr } = await runCommand(); if (error) { lastError = error; this.log.error(`${this.logPrefix}Command execution error: ${error.message}`); logToFile(`${this.logPrefix}Command execution error: ${error.message}`, 'ERROR'); if (error.stack) { this.log.debug(`${this.logPrefix}Error stack: ${error.stack}`); logToFile(`${this.logPrefix}Error stack: ${error.stack}`, 'DEBUG'); } // Retry all errors, not limited to specific types if (retryCount < maxRetries) { retryCount++; // Increase retry interval as retry count increases const waitTime = 1000 * Math.pow(1.5, retryCount - 1); this.log.warn(`${this.logPrefix}Command failed, will retry in ${waitTime}ms (attempt ${retryCount}/${maxRetries})`); logToFile(`${this.logPrefix}Command failed, will retry in ${waitTime}ms (attempt ${retryCount}/${maxRetries})`, 'RETRY'); // Wait a while before next loop await new Promise(wait => setTimeout(wait, waitTime)); continue; } else { // Max retry count reached this.log.warn(`${this.logPrefix}Max retry count ${maxRetries} reached, but still treat as command success`); logToFile(`${this.logPrefix}Max retry count ${maxRetries} reached, but still treat as command success`, 'WARNING'); break; } } else { // Command executed successfully this.log.info(`${this.logPrefix}Command executed successfully`); logToFile(`${this.logPrefix}Command executed successfully`, 'SUCCESS'); break; } } // Final state update and completion this.log.info(`${this.logPrefix}Final result: ${lastError ? 'Error but UI updated' : 'Success'}, state: ${this.currentState ? 'ON' : 'OFF'}`); logToFile(`${this.logPrefix}Final result: ${lastError ? 'Error but UI updated' : 'Success'}, state: ${this.currentState ? 'ON' : 'OFF'}`, 'FINAL'); resolve(this.currentState); }); } // Try to get device state async refreshDeviceState() { this.log.info(`${this.logPrefix}Initializing device state fetch...`); logToFile(`${this.logPrefix}Initializing device state fetch...`, 'STATE'); try { const cmdPath = path.join(__dirname, 'bot-status.mjs'); const fullCmd = `node "${cmdPath}" ${this.deviceId}`; this.log.debug(`${this.logPrefix}Executing state query: ${fullCmd}`); logToFile(`${this.logPrefix}Executing state query: ${fullCmd}`, 'QUERY'); // Try to get device state exec(fullCmd, { timeout: 10000 }, (error, stdout, stderr) => { if (error) { this.log.warn(`${this.logPrefix}Failed to get device state: ${error.message}`); logToFile(`${this.logPrefix}Failed to get device state: ${error.message}`, 'WARNING'); // Keep default state if failed to get state return; } if (stdout) { try { // 尝试解析状态信息,提取JSON部分 this.log.debug(`${this.logPrefix}Device status query result: ${stdout}`); logToFile(`${this.logPrefix}Device status query result: ${stdout}`, 'STATE'); // 使用更可靠的方法提取JSON let statusData; try { // 首先尝试直接解析整个输出 statusData = JSON.parse(stdout.trim()); } catch (initialError) { // 如果直接解析失败,尝试找到并提取JSON对象 const jsonMatch = stdout.match(/(\{[\s\S]*?\})/); if (jsonMatch && jsonMatch[1]) { try { statusData = JSON.parse(jsonMatch[1]); } catch (nestedError) { // 如果仍然解析失败,记录详细错误 this.log.error(`${this.logPrefix}Failed to parse extracted JSON: ${nestedError.message}`); logToFile(`${this.logPrefix}Failed to parse extracted JSON: ${nestedError.message}`, 'ERROR'); logToFile(`${this.logPrefix}Extracted content: ${jsonMatch[1]}`, 'ERROR'); throw nestedError; } } else { this.log.error(`${this.logPrefix}No valid JSON object found in output`); logToFile(`${this.logPrefix}No valid JSON object found in output`, 'ERROR'); throw new Error('No valid JSON object found'); } } if (statusData) { // 安全地将对象转回字符串用于日志 let jsonString; try { jsonString = JSON.stringify(statusData); this.log.info(`${this.logPrefix}Successfully parsed device status: ${jsonString}`); logToFile(`${this.logPrefix}Successfully parsed device status: ${jsonString}`, 'STATE'); } catch (stringifyError) { this.log.warn(`${this.logPrefix}Could not stringify parsed data: ${stringifyError.message}`); logToFile(`${this.logPrefix}Could not stringify parsed data: ${stringifyError.message}`, 'WARNING'); } // 获取设备当前状态 let deviceIsOn = false; // 检查并处理多种可能的状态格式 if (statusData.hasOwnProperty('isOn')) { // 直接使用isOn属性 deviceIsOn = statusData.isOn === true; this.log.debug(`${this.logPrefix}Using isOn property: ${deviceIsOn}`); } else if (statusData.hasOwnProperty('state')) { // 如果state是布尔值 if (typeof statusData.state === 'boolean') { deviceIsOn = statusData.state; this.log.debug(`${this.logPrefix}Using state as boolean: ${deviceIsOn}`); } // 如果state是字符串 else if (typeof statusData.state === 'string') { deviceIsOn = (statusData.state === 'ON' || statusData.state === 'on'); this.log.debug(`${this.logPrefix}Using state as string: ${statusData.state} -> ${deviceIsOn}`); } } // 如果没有isOn或state但有mode,可能是设备特定格式 else if (statusData.hasOwnProperty('mode') && typeof statusData.mode === 'boolean') { // 有些设备用mode表示开关状态 deviceIsOn = statusData.mode; this.log.debug(`${this.logPrefix}Using mode as state: ${deviceIsOn}`); } // 记录解析结果 this.log.info(`${this.logPrefix}Parsed device state from data: ${deviceIsOn ? 'ON' : 'OFF'}`); logToFile(`${this.logPrefix}Parsed device state from data: ${deviceIsOn ? 'ON' : 'OFF'}`, 'PARSE'); // Handle isOn property inversion if (statusData.hasOwnProperty('isOn')) { this.currentState = statusData.isOn === true; } else if (statusData.state === 'ON' || statusData.state === 'on') { this.currentState = true } else if (statusData.state === 'OFF' || statusData.state === 'off') { this.currentState = false } this.log.info(`${this.logPrefix}Initialized state set to: ${this.currentState ? 'ON' : 'OFF'}`); logToFile(`${this.logPrefix}Initialized state set to: ${this.currentState ? 'ON' : 'OFF'}`, 'STATE'); // Update cached state for characteristics this.updateCachedState(); } else { throw new Error('No valid JSON data found'); } } catch (parseError) { this.log.error(`${this.logPrefix}Failed to parse device state: ${parseError.message}`); logToFile(`${this.logPrefix}Failed to parse device state: ${parseError.message}`, 'ERROR'); } } }); } catch (e) { this.log.error(`${this.logPrefix}Error refreshing device state: ${e.message}`); logToFile(`${this.logPrefix}Error refreshing device state: ${e.message}`, 'ERROR'); } } // Update cached state for characteristics updateCachedState() { try { // Update HomeKit cached state this.switchService.updateCharacteristic(this.Characteristic.On, this.currentState); this.log.debug(`${this.logPrefix}Update HomeKit cached state to: ${this.currentState ? 'ON' : 'OFF'}`); logToFile(`${this.logPrefix}Update HomeKit cached state to: ${this.currentState ? 'ON' : 'OFF'}`, 'CACHE'); } catch (e) { this.log.error(`${this.logPrefix}Failed to update characteristic cached state: ${e.message}`); logToFile(`${this.logPrefix}Failed to update characteristic cached state: ${e.message}`, 'ERROR'); } } // 添加启动定时器的函数 startStatusCheckTimer() { // 清除已有的定时器 if (this.statusCheckTimer) { clearInterval(this.statusCheckTimer); } this.log.info(`${this.logPrefix}Status check timer started with interval ${this.statusCheckInterval}s`); logToFile(`${this.logPrefix}Status check timer started with interval ${this.statusCheckInterval}s`); // 设置新定时器 this.statusCheckTimer = setInterval(() => { this.checkDeviceStatus(); }, this.statusCheckInterval * 1000); } // 停止定时器 stopStatusCheckTimer() { if (this.statusCheckTimer) { clearInterval(this.statusCheckTimer); this.statusCheckTimer = null; this.log.info(`${this.logPrefix}Status check timer stopped`); logToFile(`${this.logPrefix}Status check timer stopped`); } } // 设备状态检查函数 async checkDeviceStatus() { if (!this.deviceId) { return; } const timestamp = new Date().toISOString(); this.log.info(`${this.logPrefix}[${timestamp}] Running scheduled status check for device: ${this.deviceId}`); logToFile(`${this.logPrefix}Running scheduled status check for device: ${this.deviceId}`); try { const cmdPath = path.join(__dirname, 'bot-status.mjs'); const fullCmd = `node "${cmdPath}" ${this.deviceId}`; this.log.debug(`${this.logPrefix}Executing state query: ${fullCmd}`); logToFile(`${this.logPrefix}Executing state query: ${fullCmd}`, 'QUERY'); // 尝试获取设备状态 exec(fullCmd, { timeout: 10000 }, (error, stdout, stderr) => { if (error) { this.log.warn(`${this.logPrefix}Failed to get device state: ${error.message}`); logToFile(`${this.logPrefix}Failed to get device state: ${error.message}`, 'WARNING'); return; } if (stdout) { try { // 尝试解析状态信息,提取JSON部分 this.log.debug(`${this.logPrefix}Device status query result: ${stdout}`); logToFile(`${this.logPrefix}Device status query result: ${stdout}`, 'STATE'); // 使用更可靠的方法提取JSON let statusData; try { // 首先尝试直接解析整个输出 statusData = JSON.parse(stdout.trim()); } catch (initialError) { // 如果直接解析失败,尝试找到并提取JSON对象 const jsonMatch = stdout.match(/(\{[\s\S]*?\})/); if (jsonMatch && jsonMatch[1]) { try { statusData = JSON.parse(jsonMatch[1]); } catch (nestedError) { // 如果仍然解析失败,记录详细错误 this.log.error(`${this.logPrefix}Failed to parse extracted JSON: ${nestedError.message}`); logToFile(`${this.logPrefix}Failed to parse extracted JSON: ${nestedError.message}`, 'ERROR'); logToFile(`${this.logPrefix}Extracted content: ${jsonMatch[1]}`, 'ERROR'); throw nestedError; } } else { this.log.error(`${this.logPrefix}No valid JSON object found in output`); logToFile(`${this.logPrefix}No valid JSON object found in output`, 'ERROR'); throw new Error('No valid JSON object found'); } } if (statusData) { // 安全地将对象转回字符串用于日志 let jsonString; try { jsonString = JSON.stringify(statusData); this.log.info(`${this.logPrefix}Successfully parsed device status: ${jsonString}`); logToFile(`${this.logPrefix}Successfully parsed device status: ${jsonString}`, 'STATE'); } catch (stringifyError) { this.log.warn(`${this.logPrefix}Could not stringify parsed data: ${stringifyError.message}`); logToFile(`${this.logPrefix}Could not stringify parsed data: ${stringifyError.message}`, 'WARNING'); } // 获取设备当前状态 let deviceIsOn = false; // 检查并处理多种可能的状态格式 if (statusData.hasOwnProperty('isOn')) { // 直接使用isOn属性 deviceIsOn = statusData.isOn === true; this.log.debug(`${this.logPrefix}Using isOn property: ${deviceIsOn}`); } else if (statusData.hasOwnProperty('state')) { // 如果state是布尔值 if (typeof statusData.state === 'boolean') { deviceIsOn = statusData.state; this.log.debug(`${this.logPrefix}Using state as boolean: ${deviceIsOn}`); } // 如果state是字符串 else if (typeof statusData.state === 'string') { deviceIsOn = (statusData.state === 'ON' || statusData.state === 'on'); this.log.debug(`${this.logPrefix}Using state as string: ${statusData.state} -> ${deviceIsOn}`); } } // 如果没有isOn或state但有mode,可能是设备特定格式 else if (statusData.hasOwnProperty('mode') && typeof statusData.mode === 'boolean') { // 有些设备用mode表示开关状态 deviceIsOn = statusData.mode; this.log.debug(`${this.logPrefix}Using mode as state: ${deviceIsOn}`); } // 记录解析结果 this.log.info(`${this.logPrefix}Parsed device state from data: ${deviceIsOn ? 'ON' : 'OFF'}`); logToFile(`${this.logPrefix}Parsed device state from data: ${deviceIsOn ? 'ON' : 'OFF'}`, 'PARSE'); // 记录当前状态 this.log.debug(`${this.logPrefix}Current device state: ${deviceIsOn ? 'ON' : 'OFF'}, HomeKit state: ${this.currentState ? 'ON' : 'OFF'}`); logToFile(`${this.logPrefix}Current device state: ${deviceIsOn ? 'ON' : 'OFF'}, HomeKit state: ${this.currentState ? 'ON' : 'OFF'}`, 'DEBUG'); // 如果状态不同,更新HomeKit if (this.currentState !== deviceIsOn) { this.log.info(`${this.logPrefix}Device state changed externally: ${deviceIsOn ? 'ON' : 'OFF'}`); logToFile(`${this.logPrefix}Device state changed externally: ${deviceIsOn ? 'ON' : 'OFF'}`, 'STATE'); // 更新本地状态 this.currentState = deviceIsOn; // 更新HomeKit界面 this.switchService.updateCharacteristic(this.Characteristic.On, deviceIsOn); this.log.info(`${this.logPrefix}Updated HomeKit state to match device: ${deviceIsOn ? 'ON' : 'OFF'}`); logToFile(`${this.logPrefix}Updated HomeKit state to match device: ${deviceIsOn ? 'ON' : 'OFF'}`, 'STATE'); } else { // 状态未变化,记录稳定状态 this.log.debug(`${this.logPrefix}Device state unchanged, remains: ${deviceIsOn ? 'ON' : 'OFF'}`); logToFile(`${this.logPrefix}Device state unchanged, remains: ${deviceIsOn ? 'ON' : 'OFF'}`, 'STATE'); } // 记录额外设备信息(如果有) if (statusData.battery) { this.log.debug(`${this.logPrefix}Device battery: ${statusData.battery}%`); logToFile(`${this.logPrefix}Device battery: ${statusData.battery}%`, 'INFO'); } } else { throw new Error('No valid JSON data found'); } } catch (parseError) { this.log.error(`${this.logPrefix}Failed to parse device state: ${parseError.message}`); logToFile(`${this.logPrefix}Failed to parse device state: ${parseError.message}`, 'ERROR'); } } }); } catch (e) { this.log.error(`${this.logPrefix}Error checking device status: ${e.message}`); logToFile(`${this.logPrefix}Error checking device status: ${e.message}`, 'ERROR'); } } }