import { DeviceDriver } from '../drivers/types'; import { sleep } from './common/element.helper'; import { execSync } from 'child_process'; export interface AutomationCondition { type: string; // e.g. "Smart Devices", "Schedules", "NFC Tag", "Outdoor Weather" device?: string; // device name if type is Smart Devices trigger?: string; // specific trigger text to select } export interface AutomationAction { type: string; // e.g. "Smart Devices", "Notifications", "Delay" device?: string; // device name if type is Smart Devices command?: string; // specific command text to select } export interface AutomationConfig { name: string; condition: AutomationCondition; action: AutomationAction; } export class AutomationHelper { private steps: string[] = []; constructor(private driver: DeviceDriver) {} private isAndroid() { return this.driver.platform === 'android'; } private async logPageElements(source?: string): Promise { const src = source || await this.driver.getSource(); if (this.isAndroid()) { const textRe = /text="([^"]{1,80})"/g; const descRe = /content-desc="([^"]{1,80})"/g; const texts: string[] = []; const descs: string[] = []; let m; while ((m = textRe.exec(src)) !== null) { if (m[1] && !texts.includes(m[1])) texts.push(m[1]); } while ((m = descRe.exec(src)) !== null) { if (m[1] && !descs.includes(m[1])) descs.push(m[1]); } console.log('Page texts:', texts.join(' | ')); if (descs.length) console.log('Page descs:', descs.join(' | ')); } return src; } private async dismissGuidePopups(): Promise { for (let i = 0; i < 5; i++) { const gotIt = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Got it")'); if (gotIt) { await this.driver.tapElement(gotIt); await sleep(1500); this.steps.push('关闭引导弹框'); } else { break; } } } async ensureAppRunning(): Promise { if (!this.isAndroid()) return; try { const src = await this.driver.getSource(); if (src.includes('com.theswitchbot.switchbot') || src.includes('SwitchBot') || src.includes('Home') || src.includes('Automation') || src.includes('Scene')) return; } catch { /* app likely crashed */ } try { execSync('adb shell am force-stop com.theswitchbot.switchbot'); await sleep(2000); execSync('adb shell am start -n com.theswitchbot.switchbot/.index.ui.SplashActivity'); await sleep(10000); await this.driver.dismissPopupIfPresent(); } catch { /* ignore */ } } async navigateToAutomationTab(): Promise { await this.ensureAppRunning(); const src = await this.driver.getSource(); if (src.includes('My Automations') && (src.includes('Recommended') || src.includes('新自动化'))) { this.steps.push('已在Automation页面'); return true; } // On creation sub-page — force restart to clear if (src.includes('Add condition') || src.includes('Create Automation') || src.includes('Add action')) { if (this.isAndroid()) { execSync('adb shell am force-stop com.theswitchbot.switchbot'); await sleep(2000); execSync('adb shell am start -n com.theswitchbot.switchbot/.index.ui.SplashActivity'); await sleep(10000); await this.driver.dismissPopupIfPresent(); } } else { await this.driver.goBackToHomepage(); await sleep(2000); await this.driver.dismissPopupIfPresent(); } let tabEl: string | null = null; if (this.isAndroid()) { tabEl = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Automations")'); if (!tabEl) tabEl = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Automations")'); } else { tabEl = await this.driver.findElementRaw('predicate string', 'label == "Automations" OR label == "Automation"'); } if (!tabEl) { this.steps.push('未找到Automations tab'); await this.logPageElements(); return false; } await this.driver.tapElement(tabEl); await sleep(3000); this.steps.push('点击Automations tab'); await this.dismissGuidePopups(); await this.driver.dismissPopupIfPresent(); const pageSrc = await this.driver.getSource(); return pageSrc.includes('My Automations') || pageSrc.includes('Recommended'); } async enterCreatePage(): Promise { if (this.isAndroid()) { const addEl = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().resourceId("com.theswitchbot.switchbot:id/addBto")'); if (!addEl) { this.steps.push('未找到Add按钮(addBto)'); return false; } await this.driver.tapElement(addEl); await sleep(5000); this.steps.push('点击Add按钮'); } else { const addEl = await this.driver.findElementRaw('predicate string', 'label == "Add" OR label == "+"'); if (!addEl) { this.steps.push('未找到Add按钮'); return false; } await this.driver.tapElement(addEl); await sleep(5000); this.steps.push('点击Add按钮'); } await this.dismissGuidePopups(); const src = await this.driver.getSource(); if (src.includes('Create Automation') || src.includes('Add condition')) { this.steps.push('进入创建自动化页面'); return true; } this.steps.push('未进入创建页面'); await this.logPageElements(src); return false; } async inputAutomationName(name: string): Promise { let nameInput: string | null = null; if (this.isAndroid()) { nameInput = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Enter Automation name")'); if (!nameInput) nameInput = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().className("android.widget.EditText").instance(0)'); } else { nameInput = await this.driver.findElementRaw('class name', 'XCUIElementTypeTextField'); } if (!nameInput) { this.steps.push('未找到名称输入框'); return false; } await this.driver.tapElement(nameInput); await sleep(500); await this.driver.clearText(nameInput); await this.driver.typeText(nameInput, name); await sleep(1000); // Hide keyboard await this.driver.goBack(); await sleep(1000); this.steps.push(`输入名称: ${name}`); return true; } async selectCondition(condition: AutomationCondition): Promise { // Click "Add condition" card on Create Automation page let condCard: string | null = null; if (this.isAndroid()) { condCard = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Add condition")'); if (!condCard) condCard = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Add condition")'); } if (!condCard) { this.steps.push('未找到Add condition卡片'); return false; } await this.driver.tapElement(condCard); await sleep(3000); await this.dismissGuidePopups(); this.steps.push('点击Add condition'); // Now on condition type selection page // Select condition type (e.g. "Smart Devices", "Schedules") let typeEl: string | null = null; if (this.isAndroid()) { typeEl = await this.driver.findElementRaw('-android uiautomator', `new UiSelector().descriptionContains("${condition.type}")`); if (!typeEl) typeEl = await this.driver.findElementRaw('-android uiautomator', `new UiSelector().text("${condition.type}")`); } if (!typeEl) { this.steps.push(`未找到条件类型: ${condition.type}`); await this.logPageElements(); return false; } await this.driver.tapElement(typeEl); await sleep(3000); this.steps.push(`选择条件类型: ${condition.type}`); // If Smart Devices, select specific device if (condition.type === 'Smart Devices' && condition.device) { let devEl: string | null = null; if (this.isAndroid()) { devEl = await this.driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${condition.device}")`); if (!devEl) devEl = await this.driver.findElementRaw('-android uiautomator', `new UiSelector().descriptionContains("${condition.device}")`); } if (devEl) { await this.driver.tapElement(devEl); await sleep(3000); this.steps.push(`选择设备: ${condition.device}`); } else { this.steps.push(`未找到设备: ${condition.device}`); await this.logPageElements(); return false; } } // Select specific trigger if provided if (condition.trigger) { let trigEl: string | null = null; if (this.isAndroid()) { trigEl = await this.driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${condition.trigger}")`); if (!trigEl) trigEl = await this.driver.findElementRaw('-android uiautomator', `new UiSelector().descriptionContains("${condition.trigger}")`); } if (trigEl) { await this.driver.tapElement(trigEl); await sleep(3000); this.steps.push(`选择触发条件: ${condition.trigger}`); } else { this.steps.push(`未找到触发条件: ${condition.trigger}`); await this.logPageElements(); } } return true; } async selectAction(action: AutomationAction): Promise { // Click "Add action" card on Create Automation page let actCard: string | null = null; if (this.isAndroid()) { actCard = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Add action")'); if (!actCard) actCard = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Add action")'); } if (!actCard) { this.steps.push('未找到Add action卡片'); return false; } await this.driver.tapElement(actCard); await sleep(3000); await this.dismissGuidePopups(); this.steps.push('点击Add action'); // Select action type let typeEl: string | null = null; if (this.isAndroid()) { typeEl = await this.driver.findElementRaw('-android uiautomator', `new UiSelector().descriptionContains("${action.type}")`); if (!typeEl) typeEl = await this.driver.findElementRaw('-android uiautomator', `new UiSelector().text("${action.type}")`); } if (!typeEl) { this.steps.push(`未找到动作类型: ${action.type}`); await this.logPageElements(); return false; } await this.driver.tapElement(typeEl); await sleep(3000); this.steps.push(`选择动作类型: ${action.type}`); // If Smart Devices, select device if (action.type === 'Smart Devices' && action.device) { let devEl: string | null = null; if (this.isAndroid()) { devEl = await this.driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${action.device}")`); if (!devEl) devEl = await this.driver.findElementRaw('-android uiautomator', `new UiSelector().descriptionContains("${action.device}")`); } if (devEl) { await this.driver.tapElement(devEl); await sleep(3000); this.steps.push(`选择设备: ${action.device}`); } else { this.steps.push(`未找到设备: ${action.device}`); await this.logPageElements(); return false; } } // Select specific command if provided if (action.command) { let cmdEl: string | null = null; if (this.isAndroid()) { cmdEl = await this.driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${action.command}")`); if (!cmdEl) cmdEl = await this.driver.findElementRaw('-android uiautomator', `new UiSelector().descriptionContains("${action.command}")`); } if (cmdEl) { await this.driver.tapElement(cmdEl); await sleep(3000); this.steps.push(`选择命令: ${action.command}`); } else { this.steps.push(`未找到命令: ${action.command}`); await this.logPageElements(); } } return true; } async saveAutomation(): Promise { let saveEl: string | null = null; if (this.isAndroid()) { saveEl = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Save")'); if (!saveEl) saveEl = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Save")'); } else { saveEl = await this.driver.findElementRaw('predicate string', 'label == "Save"'); } if (!saveEl) { // Scroll down to find Save await this.driver.scrollDown(300); await sleep(1500); if (this.isAndroid()) { saveEl = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Save")'); if (!saveEl) saveEl = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Save")'); } } if (!saveEl) { this.steps.push('未找到Save按钮'); await this.logPageElements(); return false; } await this.driver.tapElement(saveEl); await sleep(5000); this.steps.push('点击Save保存'); return true; } async createAutomation(config: AutomationConfig): Promise { // Step 1: Navigate to Automation tab const onTab = await this.navigateToAutomationTab(); if (!onTab) return false; // Step 2: Enter creation page const onCreate = await this.enterCreatePage(); if (!onCreate) return false; // Step 3: Input name await this.inputAutomationName(config.name); // Step 4: Select condition const condOk = await this.selectCondition(config.condition); if (!condOk) return false; // Step 5: Select action const actOk = await this.selectAction(config.action); if (!actOk) return false; // Step 6: Save return await this.saveAutomation(); } async deleteAutomation(name: string): Promise { const onTab = await this.navigateToAutomationTab(); if (!onTab) return false; // Find automation by name let targetEl: string | null = null; if (this.isAndroid()) { targetEl = await this.driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${name}")`); if (!targetEl) targetEl = await this.driver.findElementRaw('-android uiautomator', `new UiSelector().descriptionContains("${name}")`); } else { targetEl = await this.driver.findElementRaw('predicate string', `label CONTAINS "${name}"`); } if (!targetEl) { this.steps.push(`未找到自动化: ${name}`); await this.logPageElements(); return false; } // Enter automation detail await this.driver.tapElement(targetEl); await sleep(3000); this.steps.push(`点击自动化: ${name}`); // Look for Delete/Remove button (may need to scroll) let delEl: string | null = null; if (this.isAndroid()) { delEl = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")'); if (!delEl) delEl = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Remove")'); if (!delEl) delEl = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().description("Delete")'); } if (!delEl) { await this.driver.scrollDown(500); await sleep(2000); if (this.isAndroid()) { delEl = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")'); if (!delEl) delEl = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Remove")'); } } if (!delEl) { this.steps.push('未找到Delete按钮'); await this.logPageElements(); await this.driver.goBack(); await sleep(2000); return false; } await this.driver.tapElement(delEl); await sleep(3000); this.steps.push('点击Delete'); // Confirm deletion dialog if (this.isAndroid()) { let cfm: string | null = null; cfm = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Confirm")'); if (!cfm) cfm = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().text("OK")'); if (!cfm) cfm = await this.driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")'); if (cfm) { await this.driver.tapElement(cfm); await sleep(3000); this.steps.push('确认删除'); } } return true; } getSteps(): string[] { return [...this.steps]; } clearSteps(): void { this.steps = []; } }