import { DeviceDriver } from '../../drivers/types'; import { sleep, waitForElement, waitForSource } from './element.helper'; import { ensureHomeTab, navigateToAddPage, navigateToManageScenes, dismissGuideOverlay, dismissSaveConfirmCancel } from './navigation.helper'; export interface SceneConfig { name: string; deviceKeyword: string; action: string; } export async function createScene(driver: DeviceDriver, config: SceneConfig): Promise { const { name, deviceKeyword, action } = config; const opened = await navigateToAddPage(driver, 'Scene'); if (!opened) return false; await dismissGuideOverlay(driver); // 首进场景页可能有引导浮层,挡住 Add action if (driver.platform === 'android') { const addAction = await driver.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Add action")'); if (!addAction) { console.log('FAIL: no Add action'); return false; } await driver.tapElement(addAction); await sleep(2000); const smartDevices = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Smart Devices")'); if (!smartDevices) { console.log('FAIL: no Smart Devices'); return false; } await driver.tapElement(smartDevices); // 找设备(滚动查找:设备列表较长,Bot 等靠后设备需下滑才可见) let deviceEl: string | null = null; for (let i = 0; i < 15 && !deviceEl; i++) { deviceEl = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${deviceKeyword}")`); if (!deviceEl) { await driver.scrollDown(400); await sleep(700); } } if (!deviceEl) { console.log(`FAIL: no ${deviceKeyword} in device list`); return false; } await driver.tapElement(deviceEl); await sleep(2000); let actionEl = await driver.findElementRaw('-android uiautomator', `new UiSelector().text("${action}")`); if (!actionEl) { actionEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Presses once")'); } if (!actionEl) { console.log('FAIL: no action option'); return false; } await driver.tapElement(actionEl); await sleep(2000); const editText = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Enter Scene name")'); if (editText) { await driver.tapElement(editText); await sleep(300); await driver.typeText(editText, name); await sleep(300); await driver.goBack(); await sleep(500); } const saveEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Save")'); if (!saveEl) { console.log('FAIL: no Save'); return false; } await driver.tapElement(saveEl); await sleep(2000); await dismissSaveConfirmCancel(driver); // Save 后弹"立即执行一次?"确认框 → 点 Cancel(场景已保存) await sleep(1000); return true; } // iOS const addAction = await driver.findElementRaw('predicate string', 'name BEGINSWITH "Add action" AND name CONTAINS "E.g."'); if (!addAction) { console.log('FAIL: no Add action'); return false; } const aaRect = await driver.getElementRect(addAction); await driver.tap(aaRect.x + aaRect.width / 2, aaRect.y + aaRect.height / 2); const smartDevices = await waitForElement(driver, 'predicate string', 'name CONTAINS "Smart Devices"', 6000); if (!smartDevices) { console.log('FAIL: no Smart Devices'); return false; } await driver.tapElement(smartDevices); let picked = false; { // iOS 设备列表很长(Bot 等靠后设备 y 远超窗口,如 y=1885 在 844 窗口外)。用**首页同款滑动法 driver.scrollDown** // (居中 0.1s 快速甩动,实测稳定滚动、不误点行)循环查找:**仅当目标在可视区(窗口高内)才点中心**, // 找到即停、到底(页面签名不再变化)即停。修复旧版"找到 off-screen 元素仍点 y=1885 屏外"导致没选中的 bug。 const { height: winH } = await driver.getWindowSize().catch(() => ({ height: 844 })); let lastSig = ''; for (let r = 0; r < 14 && !picked; r++) { const el = await driver.findElementRaw('predicate string', `name CONTAINS "${deviceKeyword}" OR label CONTAINS "${deviceKeyword}"`).catch(() => null); if (el) { const rect = await driver.getElementRect(el).catch(() => null); if (rect && rect.width > 0 && rect.y > winH * 0.11 && rect.y + rect.height < winH * 0.95) { await driver.tap(Math.round(rect.x + rect.width / 2), Math.round(rect.y + rect.height / 2)); picked = true; break; } } const sig = (await driver.getSource()).slice(-2500); // 到底检测:滚动前后尾部签名不变 = 已到底 if (sig === lastSig) break; lastSig = sig; await driver.scrollDown(500); await sleep(900); } } if (!picked) { console.log(`FAIL: no ${deviceKeyword}(可视区)`); return false; } const actionEl = await waitForElement(driver, 'name', action, 5000); if (!actionEl) { // Fallback: if Bot is in Press mode, "Turns off/on" won't exist const pressEl = await driver.findElementRaw('name', 'Presses once'); if (!pressEl) { console.log('FAIL: no action ' + action); return false; } await driver.tapElement(pressEl); } else { await driver.tapElement(actionEl); } await sleep(1500); const tfs = await driver.findElementsRaw('class name', 'XCUIElementTypeTextField'); if (tfs.length > 0) { await driver.tapElement(tfs[0]); await sleep(300); await driver.typeText(tfs[0], name); await sleep(300); const done = await driver.findElementRaw('name', 'Done'); if (done) { await driver.tapElement(done); } else { await driver.tap(195, 400); } await sleep(1000); } const saveEl = await driver.findElementRaw('name', 'Save'); if (!saveEl) { console.log('FAIL: no Save'); return false; } await driver.tapElement(saveEl); await sleep(2000); await dismissSaveConfirmCancel(driver); // Save 后弹"立即执行一次?"确认框 → 点 Cancel(场景已保存) await sleep(1000); return true; } export async function executeSceneFromHomepage(driver: DeviceDriver, sceneName: string): Promise { if (driver.platform === 'android') { await driver.goBackToHomepage(); await sleep(500); try { const homeTab = await driver.findElementRaw('accessibility id', 'Home'); if (homeTab) { await driver.tapElement(homeTab); await sleep(1000); } } catch { /* stale */ } for (let i = 0; i < 5; i++) { const sceneEl = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${sceneName}")`); if (sceneEl) { await driver.tapElement(sceneEl); await sleep(1500); return true; } await driver.scrollDown(300); await sleep(800); } return false; } // iOS:手动场景作为可点条目出现在首页。用首页滑动法循环查找,**仅可视区命中才点**(旧版 tapElement 会点屏外元素),找到即停/到底即停。 await driver.goBackToHomepage().catch(() => {}); await ensureHomeTab(driver); await sleep(800); const { height: winH } = await driver.getWindowSize().catch(() => ({ height: 844 })); let lastSig = ''; for (let i = 0; i < 14; i++) { const el = await driver.findElementRaw('predicate string', `name CONTAINS "${sceneName}" OR label CONTAINS "${sceneName}"`).catch(() => null); if (el) { const rect = await driver.getElementRect(el).catch(() => null); if (rect && rect.width > 0 && rect.y > winH * 0.08 && rect.y + rect.height < winH * 0.95) { await driver.tap(Math.round(rect.x + rect.width / 2), Math.round(rect.y + rect.height / 2)); await sleep(1500); return true; } } const sig = (await driver.getSource()).slice(-2500); if (sig === lastSig) break; lastSig = sig; await driver.scrollDown(500); await sleep(900); } return false; } export async function deleteScene(driver: DeviceDriver, sceneName: string): Promise { if (driver.platform === 'android') { try { const homeTab = await driver.findElementRaw('accessibility id', 'Home'); if (homeTab) { await driver.tapElement(homeTab); await sleep(1000); } } catch { /* stale */ } const moreBtn = await driver.findElementRaw('id', 'com.theswitchbot.switchbot:id/moreBto'); if (!moreBtn) return false; await driver.tapElement(moreBtn); await sleep(1500); const manageEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Manage Scenes")'); if (!manageEl) return false; await driver.tapElement(manageEl); let sceneEl: string | null = null; for (let i = 0; i < 15; i++) { await sleep(1000); sceneEl = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${sceneName}")`); if (sceneEl) break; } if (!sceneEl) return false; await driver.tapElement(sceneEl); await sleep(2000); let delEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")'); if (!delEl) { await driver.scrollDown(300); await sleep(500); delEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")'); } if (!delEl) return false; await driver.tapElement(delEl); await sleep(2000); const confirmEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Delete")'); if (confirmEl) { await driver.tapElement(confirmEl); await sleep(1500); } return true; } // iOS await driver.tap(347, 48); await sleep(1500); const manageEl = await driver.findElementRaw('name', 'Manage Scenes'); if (!manageEl) return false; await driver.tapElement(manageEl); await sleep(2000); const source = await driver.getSource(); if (!source.includes(sceneName)) return false; const myScenesEl = await driver.findElementRaw('name', 'My Scenes'); if (!myScenesEl) return false; const msRect = await driver.getElementRect(myScenesEl); await driver.tap(195, msRect.y + msRect.height + 40); await sleep(2000); const editSource = await driver.getSource(); if (!editSource.includes('Edit Scene')) return false; const delEl = await driver.findElementRaw('name', 'Delete'); if (!delEl) return false; await driver.tapElement(delEl); await sleep(1500); const delConfirm = await driver.findElementRaw('name', 'Delete'); if (delConfirm) { await driver.tapElement(delConfirm); await sleep(1500); } return true; } export async function verifySceneInLogs(driver: DeviceDriver, sceneName: string): Promise { if (driver.platform === 'android') { const navigated = await navigateToManageScenes(driver); if (!navigated) return false; await sleep(1500); // 场景日志入口:Manage Scenes 右上角有两个图标(左=Create,右=日志)。id 会命中 Create,故直接点最右图标坐标(1080×2280 约 1010,175)→ 菜单 "Scene Logs"。 await driver.tap(1010, 175); await sleep(1500); const logsEl = await driver.findElementRaw('-android uiautomator', 'new UiSelector().text("Scene Logs")'); if (!logsEl) { console.log('未进入 Scene Logs 菜单'); return false; } await driver.tapElement(logsEl); await sleep(2500); const source = await driver.getSource(); const has = source.includes(sceneName); // 日志页含该场景名 = 已执行(记录) console.log(`Scene Logs 有执行记录(${sceneName}): ${has}`); return has; } // iOS: navigate to Manage Scenes await driver.tap(347, 48); await sleep(1500); const manageEl = await driver.findElementRaw('name', 'Manage Scenes'); if (!manageEl) return false; await driver.tapElement(manageEl); await sleep(2000); // Find the scene and tap to enter detail const sceneEl = await driver.findElementRaw('predicate string', `name CONTAINS "${sceneName}"`); if (!sceneEl) return false; await driver.tapElement(sceneEl); await sleep(2000); // Tap the top-right button to see execution history/status const navBar = await driver.findElementRaw('class name', 'XCUIElementTypeNavigationBar'); if (navBar) { const navRect = await driver.getElementRect(navBar); // Top-right corner of navigation bar await driver.tap(navRect.x + navRect.width - 30, navRect.y + navRect.height / 2); await sleep(2000); } const source = await driver.getSource(); return source.includes('Success') || source.includes('success') || source.includes('Executed') || source.includes(sceneName); } /** * 通用动作选择(场景/自动化共用):在"Add action 后的动作来源页"点 Smart Devices → 找设备 → 选动作。 * 兼容 Bot 两模式(按压=Presses once / 开关=Turns off/Turns on)与其它设备动作。 * 调用前:已点过 "Add action"(处于动作来源选择页)。 * @param actionPref 优先尝试的动作文本(如 'Turns off' / 'Presses once');找不到按通用顺序兜底。 */ export async function selectSmartDeviceAction( driver: DeviceDriver, deviceKeyword: string, actionPref?: string ): Promise { const tap = async (sel: string, val: string) => { const el = await driver.findElementRaw(sel, val); if (el) { await driver.tapElement(el); return true; } return false; }; if (!(await tap('-android uiautomator', 'new UiSelector().text("Smart Devices")'))) { console.log('FAIL: no Smart Devices'); return false; } await sleep(2000); // 找设备(滚动) let dev: string | null = null; for (let i = 0; i < 12 && !dev; i++) { dev = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${deviceKeyword}")`); if (!dev) { await driver.scrollDown(400); await sleep(700); } } if (!dev) { console.log(`FAIL: no device ${deviceKeyword}`); return false; } await driver.tapElement(dev); await sleep(2000); // 选动作:优先 actionPref,再兜底 Presses once / Turns off / Turns on const candidates = [actionPref, 'Presses once', 'Turns off', 'Turns on'].filter(Boolean) as string[]; for (const a of candidates) { if (await tap('-android uiautomator', `new UiSelector().text("${a}")`)) { console.log(`选动作: ${a}`); await sleep(2000); return true; } } console.log('FAIL: no action option'); return false; }