/** * 自动化(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 { 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 { 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 { 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 { 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 { 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 { 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 { // 先退出可能的子页(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 { 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 { 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; }