AI_UIAutomation/utils/automation-helper.ts

478 lines
17 KiB
TypeScript

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<string> {
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<void> {
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<void> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
// 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<boolean> {
// 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<boolean> {
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<boolean> {
// 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<boolean> {
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 = []; }
}