import { DeviceDriver } from '../../drivers/types'; import { sleep, waitForElement, waitForSource } from './element.helper'; import { ensureHomeTab, navigateToAddPage, navigateToManageScenes, dismissGuideOverlay } 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); let deviceEl: string | null = null; for (let i = 0; i < 15; i++) { await sleep(1000); deviceEl = await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${deviceKeyword}")`); if (deviceEl) break; } 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(3000); 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); const deviceEl = await waitForElement(driver, 'predicate string', `name CONTAINS "${deviceKeyword}"`, 6000); if (!deviceEl) { console.log(`FAIL: no ${deviceKeyword}`); return false; } await driver.tapElement(deviceEl); 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(3000); 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 const source = await driver.getSource(); if (!source.includes(sceneName)) { await driver.scrollUp(200); await sleep(800); } const sceneEl = await driver.findElementRaw('predicate string', `name CONTAINS "${sceneName}"`); if (sceneEl) { await driver.tapElement(sceneEl); await sleep(1500); return true; } 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; }