import { describe, it, beforeAll, afterAll, expect } from 'vitest'; import { DeviceDriver } from '../../drivers/types'; import { createDriver } from '../../drivers/factory'; import { TestReporter } from '../../utils/test-reporter'; import { sleep, addDeviceWithSerialPairing, findDeviceNameOnHomepage, saveAddedDevice, configureHubWifi, resetBluetooth, enterPairingMode, } from '../../utils/common'; import { getWifiCredentials } from '../../config/wifi.config'; import * as dotenv from 'dotenv'; import * as path from 'path'; dotenv.config({ path: path.resolve(__dirname, '../../.env') }); // Relay Switch 1PM:WiFi 产品(relay ch48),添加目录在 "Power & Switch" 分组、与 Plug Mini 同组。 // Relay Switch 有 1PM/1/2PM 多个物理型号,**共用添加用例 ONES:302288,取 1PM 为代表**(见 ones-anchor.helper)。 // 流程:选品类 "Relay Switch" → 二级选型号 "Relay Switch 1PM" → 跳接线引导 → 长按配对键(ch48,2s)→ 连接 → WiFi 配网。 const CATEGORY = process.env.RELAY_SWITCH_CATEGORY || 'Relay Switch'; const SUB_MODEL = process.env.RELAY_SWITCH_SUBMODEL || 'Relay Switch 1'; // 二级型号(目录里点选) const deviceName = process.env.RELAY_SWITCH_DEVICE || 'Relay Switch 1'; // ⚠️ 实测物理设备(ch48)上报名为 "Relay Switch 1 32"(非 1PM)→ 用 "Relay Switch 1" 前缀匹配真实名,否则首页校验找不到 const NAME_PATTERN = process.env.RELAY_SWITCH_PATTERN || 'Relay Switch 1\\s*[0-9A-Za-z]{0,4}'; // 继电器配对键设备(通道):relay_switch=ch48(Relay Switch 1) / relay_switch_1pm=ch59 / relay_switch_2pm=ch58 const RELAY_DEVICE = process.env.RELAY_SWITCH_RELAY || 'relay_switch'; const ADD_ANCHOR = '[P0][ONES:302288]'; describe('RelaySwitch Connect - 添加Relay Switch(串口配对 + WiFi配网)', () => { let driver: DeviceDriver; let reporter: TestReporter; beforeAll(async () => { driver = createDriver(); await driver.createSession(); reporter = new TestReporter('RelaySwitch_Connect', driver.platform.toUpperCase()); }); afterAll(async () => { reporter.generate(); await driver.destroySession(); }); it(`${ADD_ANCHOR} 通过BLE+WiFi添加Relay Switch`, { timeout: 360000 }, async () => { const start = Date.now(); const wifi = getWifiCredentials(); try { const existing = await findDeviceNameOnHomepage(driver, NAME_PATTERN); if (existing) { console.log(`${existing}已在首页,跳过重新添加`); saveAddedDevice('relay_switch', existing); reporter.record(`${ADD_ANCHOR} 添加Relay Switch`, 'SKIP', Date.now() - start, `${existing}已存在`); return; } // BLE 配对前复位手机蓝牙(关→开),根治偶发配对/连接不上 // await resetBluetooth(driver); // 串口指令(继电器按键)放在**添加流程之前**:先让 Relay Switch 进入广播(~10min 窗口), // 确保后续 Connect Device 扫描能发现它。流程内不再按(skipInitialPress),避免重复按把设备切出广播。 console.log('[RelaySwitch] 流程前按继电器配对键,让设备进入广播...'); await enterPairingMode(RELAY_DEVICE).catch((e) => console.log('relay 预按键失败:', e.message)); await sleep(2500); const result = await addDeviceWithSerialPairing(driver, { categoryName: CATEGORY, categoryScrollHint: 0, deviceKeyword: deviceName, relayDevice: RELAY_DEVICE, // ch48(默认) / ch59(1PM) / ch58(2PM) registryCategory: RELAY_DEVICE, namePattern: NAME_PATTERN, repressOnRetry: false, skipInitialPress: true, // 已在流程前按过 → 配对循环内不再按 pairRetries: 2, pairPollIters: 16, // 选 "Relay Switch" 后:① 二级选型号(默认 Relay Switch 1PM)② 接线设置引导页(AC/DC等)点 "Skip this guide" 跳过 // → 到连接页(按键2s + Connect Device,由 helper 配对+tapNext 处理) beforePairing: async (d) => { await sleep(1500); // 打印二级型号目录(防型号名与实际不符;1PM/1/2PM 等多型号都在此页)。两端通用:Android text= / iOS name=label= const catTexts = [...new Set((await d.getSource()).match(/(?:text|name|label)="[^"]*Relay[^"]*"/g) || [])].join(' | '); console.log('[RelaySwitch] 二级型号目录:', catTexts); // 选 SUB_MODEL:**精确等值匹配**(text() 全等)——目录里 "Relay Switch 1PM" 排在 "Relay Switch 1" 之前, // 若用 "Relay Switch 1.*" 会贪婪命中 1PM(选错型号→扫描签名不符→扫不到)。故先 exact,再退回全等正则。 let sub = await d.findElementRaw('-android uiautomator', `new UiSelector().text("${SUB_MODEL}")`).catch(() => null) || await d.findElementRaw('-android uiautomator', `new UiSelector().textMatches("(?i)${SUB_MODEL}")`).catch(() => null); if (sub) { await d.tapElement(sub); console.log(`[RelaySwitch] 选型号 ${SUB_MODEL}(精确)`); await sleep(2000); } else console.log(`[RelaySwitch] ⚠️未找到型号 ${SUB_MODEL}(看上面目录确认真实名)`); if (process.env.RS_DUMP) { const s = await d.getSource(); console.log('[RS_DUMP] 选型号后页面文案:', [...new Set((s.match(/(?:text|content-desc)="[^"]{2,32}"/g) || []))].slice(0, 30).join(' | ')); } // 2PM(可能其他型号)多一步**选应用/开关类型**:"Switch & Socket"(开关插座) vs "Cover & Shade"(窗帘卷帘)。 // 开关走 "Switch & Socket";其他型号无此页则跳过。 for (let i = 0; i < 3; i++) { const st = await d.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Switch & Socket")').catch(() => null) || await d.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Switch &")').catch(() => null); if (!st) break; await d.tapElement(st); console.log('[RelaySwitch] 选开关类型 Switch & Socket'); await sleep(2000); // 选完可能有 Next/Confirm const nx = await d.findElementRaw('-android uiautomator', 'new UiSelector().textMatches("Next|Confirm|OK|Continue")').catch(() => null); if (nx) { await d.tapElement(nx); await sleep(1500); } break; } for (let i = 0; i < 3; i++) { const skip = await d.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Skip this guide")').catch(() => null); if (!skip) break; await d.tapElement(skip); await sleep(2000); } }, // 连接后流程(用户方案):点 Connect Device 发起扫描 → Select Device 没扫到 → 返回上一页 → 再点 Connect Device 重扫,**循环 3 次**。 // 设备已在流程前按继电器进入广播(~10min),3 轮重扫应能发现。扫到 → 选中 → Next → 配网。 postConnectSteps: async (d) => { const tapByRect = async (el: string) => { const r = await d.getElementRect(el).catch(() => null); if (r && r.width > 0) { await d.tap(Math.round(r.x + r.width / 2), Math.round(r.y + r.height / 2)); return true; } await d.tapElement(el).catch(() => {}); return true; }; const findConnectDevice = async () => await d.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Connect Device")').catch(() => null) || await d.findElementRaw('-android uiautomator', 'new UiSelector().textContains("Connect Device")').catch(() => null) || (d.platform === 'ios' ? await d.findElementRaw('name', 'Connect Device').catch(() => null) : null); const findScannedDev = async () => // 扫描列表里的设备项(实测名如 "Relay Switch 1 32") await d.findElementRaw('-android uiautomator', 'new UiSelector().textMatches("(?i)Relay Switch [0-9].*")').catch(() => null) || (d.platform === 'ios' ? await d.findElementRaw('predicate string', 'name CONTAINS "Relay Switch 1"').catch(() => null) : null); let selected = false; for (let round = 0; round < 3 && !selected; round++) { // a) 在 Connect Device 页 → 点它发起 BLE 扫描 const cd = await findConnectDevice(); if (cd) { await tapByRect(cd); console.log(`[RelaySwitch] 点 Connect Device 发起扫描(第${round + 1}/3 轮)`); await sleep(4000); } // b) 等扫描结果:出现设备项=选中;No device found=本轮失败;已到配网=直接成功 for (let t = 0; t < 10 && !selected; t++) { const s = await d.getSource(); const dev = await findScannedDev(); if (dev && /Select Device/i.test(s)) { await tapByRect(dev); console.log(`[RelaySwitch] 扫到设备,选中(第${round + 1} 轮)`); await sleep(1500); selected = true; break; } if (/Configure Wi-?Fi|Wi-?Fi Settings|输入设备名/i.test(s)) { selected = true; break; } if (/No device found/i.test(s)) { console.log(`[RelaySwitch] 第${round + 1} 轮 No device found`); break; } await sleep(2000); } if (selected) break; // c) 没扫到 → 返回上一页(回 Connect Device 页),下一轮重扫 if (round < 2) { console.log(`[RelaySwitch] 第${round + 1} 轮未扫到,返回上一页重扫`); await d.goBack().catch(() => {}); await sleep(2500); } } if (!selected) console.log('[RelaySwitch] ⚠️3 轮重扫仍未发现设备'); // 选中后点 Next → 进配网 const n = await d.findElementRaw('-android uiautomator', 'new UiSelector().text("Next")').catch(() => null); if (n) { await tapByRect(n); await sleep(3500); } await configureHubWifi(d, wifi); }, connectionKeywords: [ 'Select Device', 'Configure Wi-Fi', 'Wi-Fi Settings', 'Connecting', 'Pick a room', 'Done', 'added successfully', 'Added successfully', 'Initial Setup', ], }); expect(result).toBe(true); const elapsed = ((Date.now() - start) / 1000).toFixed(1); reporter.record(`${ADD_ANCHOR} 添加Relay Switch`, 'PASS', Date.now() - start, `添加成功(WiFi:${wifi.ssid}), 耗时${elapsed}s`); } catch (e: any) { const ss = await driver.screenshot().catch(() => ''); reporter.record(`${ADD_ANCHOR} 添加Relay Switch`, 'FAIL', Date.now() - start, e.message, ss); throw e; } }); });