AI_UIAutomation/utils/common/automation.helper.ts

199 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 自动化(Automation)通用流程 helper —— Android 实测(2026-06)。
* 自动化 = 条件(Schedules 定时)+ 动作(复用 scene 的 selectSmartDeviceAction)。
* 入口: Automations tab → Create(addBto)→ dismissGuideOverlay(首次引导)
* 条件: Add condition → Schedules → 时间轮设目标时间 → Save
* 动作: Add action → selectSmartDeviceAction(设备, 动作)
* 命名+Save → 到点触发 → 右上"..."→ Automation Logs 验证执行 → Edit Automation→Delete 删除
*
* 时间轮(1080×2280):小时列 x≈425、分钟列 x≈650,选中带中心 y≈1908,1 格=上滑 95px(慢滑,无惯性)。
*/
import { DeviceDriver } from '../../drivers/types';
import { sleep } from './element.helper';
import { dismissGuideOverlay } from './navigation.helper';
import { selectSmartDeviceAction } from './scene.helper';
const HOUR_X = 425;
const MIN_X = 650;
const WHEEL_TOP = 1855; // 上滑终点(中心上方一格)
const WHEEL_MID = 1950; // 上滑起点
async function tapText(driver: DeviceDriver, t: string): Promise<boolean> {
const el = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${t}")`);
if (el) { await driver.tapElement(el); return true; }
return false;
}
async function tapContains(driver: DeviceDriver, t: string): Promise<boolean> {
const el = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${t}")`);
if (el) { await driver.tapElement(el); return true; }
return false;
}
async function tapId(driver: DeviceDriver, id: string): Promise<boolean> {
const el = await driver.findElementRaw('id', `com.theswitchbot.switchbot:id/${id}`);
if (el) { await driver.tapElement(el); return true; }
return false;
}
/** 在某一列(x)上滑 n 格(n>0 增大/上滑;n<0 减小/下滑)。1 格 95px。 */
async function spinWheel(driver: DeviceDriver, x: number, n: number): Promise<void> {
const up = n > 0;
for (let i = 0; i < Math.abs(n); i++) {
if (up) await driver.swipe(x, WHEEL_MID, x, WHEEL_TOP, 0.5);
else await driver.swipe(x, WHEEL_TOP, x, WHEEL_MID, 0.5);
await sleep(250);
}
}
/**
* 设定 Schedules 时间轮到 targetH:targetM。先读 "Time HH:MM >" 行当前值作起点(新自动化默认 08:00),
* 打开时间轮 → 小时/分钟列各按差值就近方向滑动 → OK。
*/
export async function setScheduleTime(driver: DeviceDriver, targetH: number, targetM: number): Promise<void> {
const src = await driver.getSource();
const m = src.match(/text="(\d{2}):(\d{2})"/);
const startH = m ? parseInt(m[1], 10) : 8;
const startM = m ? parseInt(m[2], 10) : 0;
// 打开时间轮:点 "Time HH:MM" 行(用当前时间值文本定位)
const timeText = `${String(startH).padStart(2, '0')}:${String(startM).padStart(2, '0')}`;
if (!(await tapText(driver, timeText))) await tapContains(driver, 'Time');
await sleep(1500);
// 就近方向步数(小时 mod 24,分钟 mod 60)
const upH = ((targetH - startH) % 24 + 24) % 24;
const hSteps = upH <= 12 ? upH : -(24 - upH);
const upM = ((targetM - startM) % 60 + 60) % 60;
const mSteps = upM <= 30 ? upM : -(60 - upM);
await spinWheel(driver, HOUR_X, hSteps);
await spinWheel(driver, MIN_X, mSteps);
await sleep(500);
await tapText(driver, 'OK');
await sleep(1500);
}
export interface AutomationConfig {
name: string;
deviceKeyword: string; // 动作设备(如 'Bot 14' / 'Curtain 89')
targetHour: number; // 定时小时(由调用方按设备当前时间+偏移算出)
targetMinute: number;
action?: string; // 优先动作文本(Turns off / Presses once 等)
}
/** 创建定时自动化:条件=Schedules(targetH:targetM)+ 动作=设备动作。返回是否创建成功(出现在 My Automations)。 */
export async function createAutomation(driver: DeviceDriver, cfg: AutomationConfig): Promise<boolean> {
await driver.dismissPopupIfPresent().catch(() => {}); // 清启动 Tips/SmartThings 等弹框
const auto = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Automations")');
if (auto) { await driver.tapElement(auto); await sleep(2500); }
await driver.dismissPopupIfPresent().catch(() => {});
if (!(await tapId(driver, 'addBto'))) { console.log('FAIL: no Create(addBto)'); return false; }
await sleep(2500);
await dismissGuideOverlay(driver); // 首次引导浮层
// 条件:Add condition → Schedules → 时间轮 → Save。条件页可能稍慢/有引导,重试等待。
let onCond = false;
for (let i = 0; i < 4 && !onCond; i++) {
await dismissGuideOverlay(driver);
await tapContains(driver, 'Add condition');
await sleep(2000);
for (let j = 0; j < 6; j++) {
if ((await driver.getSource()).includes('Schedules')) { onCond = true; break; }
await sleep(800);
}
}
if (!onCond) { console.log('FAIL: 条件类型页无 Schedules'); return false; }
await tapText(driver, 'Schedules');
await sleep(2000);
await setScheduleTime(driver, cfg.targetHour, cfg.targetMinute);
await tapText(driver, 'Save'); // Schedules 页 Save
await sleep(2500);
// 动作:Add action → Smart Devices → 设备 → 动作
await tapContains(driver, 'Add action');
await sleep(2000);
if (!(await selectSmartDeviceAction(driver, cfg.deviceKeyword, cfg.action))) return false;
// 命名 + Save
const ed = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Enter Automation name")');
if (ed) { await driver.tapElement(ed); await sleep(300); await driver.typeText(ed, cfg.name); await sleep(300); try { await driver.goBack(); } catch { /**/ } }
await tapText(driver, 'Save');
await sleep(3500);
await driver.dismissPopupIfPresent();
// 校验:回 My Automations 列表重试查名(Save 后可能先 Loading/停详情页,不能立即判)
let ok = false;
for (let i = 0; i < 6 && !ok; i++) {
const a = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Automations")');
if (a) { await driver.tapElement(a); await sleep(2000); }
ok = (await driver.getSource()).includes(cfg.name);
if (!ok) await sleep(2000);
}
console.log(`创建自动化 ${cfg.name}: ${ok}`);
return ok;
}
/** 打开某自动化(确保在 My Automations 列表后再点名字,避免在日志页点到日志条目)。 */
async function openAutomation(driver: DeviceDriver, name: string): Promise<boolean> {
// 先退出可能的子页(Automation Logs / 详情),回到能看到底部导航的页
for (let i = 0; i < 3; i++) {
const s = await driver.getSource();
if (s.includes('Automation Logs') || s.includes('Scene Logs') || s.includes('Edit Automation View')) {
await driver.goBack(); await sleep(1000);
} else break;
}
const auto = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Automations")');
if (auto) { await driver.tapElement(auto); await sleep(2000); }
// 确认在 My Automations 列表页(非日志页)再点名字
if (!(await driver.getSource()).includes('My Automations')) {
const auto2 = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Automations")');
if (auto2) { await driver.tapElement(auto2); await sleep(2000); }
}
return tapContains(driver, name);
}
/**
* 验证自动化已执行:**自动化管理页(My Automations)右上角图标 → 菜单 "Automation Logs"** → 看是否有执行记录。
* 注意:日志入口在管理页右上角(全局日志),不是自动化详情页。
*/
export async function verifyAutomationExecuted(driver: DeviceDriver, name: string): Promise<boolean> {
const auto = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Automations")');
if (auto) { await driver.tapElement(auto); await sleep(2500); }
// 右上角图标 → 菜单
const ic = await driver.findElementRaw('id', 'com.theswitchbot.switchbot:id/top_bar_right_icon');
if (ic) await driver.tapElement(ic); else await driver.tap(1000, 180);
await sleep(1500);
if (!(await tapText(driver, 'Automation Logs'))) { console.log('未进入 Automation Logs'); return false; }
await sleep(2500);
const src = await driver.getSource();
// 有该自动化名的记录,或日志非空(非 "No logs / 暂无")
const hasLog = src.includes(name) || (!src.includes('No logs') && !src.includes('暂无') && !src.includes('No Logs'));
console.log(`Automation Logs 有执行记录(${name}): ${hasLog}`);
return hasLog;
}
/** 删除自动化:打开 → Edit Automation 的 Delete 按钮 → 确认框 Delete。返回是否已删除。 */
export async function deleteAutomation(driver: DeviceDriver, name: string): Promise<boolean> {
if (!(await openAutomation(driver, name))) return true;
await sleep(2000);
// Edit Automation 页底部/右上的 Delete
if (!(await tapText(driver, 'Delete'))) {
const del = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Delete")');
if (del) await driver.tapElement(del);
}
// 等确认框("Cancel | Delete")出现,点对话框里的 Delete(最后一个)
for (let i = 0; i < 6; i++) {
const dels = await driver.findElementsRaw('-android uiautomator', 'new UiSelector().text("Delete")');
const src = await driver.getSource();
if (src.includes('Cancel') && dels.length) { await driver.tapElement(dels[dels.length - 1]); break; }
await sleep(800);
}
await sleep(3000);
await driver.dismissPopupIfPresent();
// gone 校验:回 My Automations 列表查名
const a = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Automations")');
if (a) { await driver.tapElement(a); await sleep(2000); }
const gone = !(await driver.getSource()).includes(name);
console.log(`删除自动化 ${name}: ${gone ? '已删' : '仍在'}`);
return gone;
}