199 lines
9.7 KiB
TypeScript
199 lines
9.7 KiB
TypeScript
/**
|
||
* 自动化(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;
|
||
}
|