diff --git a/README.md b/README.md index 39a93fe..5674a9c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,9 @@ # miot_x -`miot_x` 是一个基于 Bun + TypeScript 的米家极客版工作流编排实验项目。目标是用 TypeScript 代码描述简单工作流,再编译、校验并打包成米家极客版可导入的 `.bak` 备份文件。 +`miot_x` 是一个基于 Bun + TypeScript 的米家极客版工作流编排项目。用 TypeScript 代码描述工作流规则,编译并打包成米家极客版可导入的 `.bak` 备份文件。 ## 当前范围 -第一版只验证“编程化开发工作流”的可行性,不做纯文本 DSL,也不做完整 MIoT 元数据智能解析。 - 当前支持: - 设备属性触发和事件触发,生成 `deviceInput` @@ -15,34 +13,38 @@ - 简单逻辑组合,生成 `logicAnd`、`logicOr`、`logicNot` - 延时动作,生成 `delay` - 注释节点,生成 `nop` -- `.bak` 解包、打包、合并和校验 +- `.bak` 解包、打包和校验 +- 从中枢网关抓取设备清单 当前不支持: - 纯文本 DSL 或自定义 parser - 中文能力名自动匹配,例如 `light.power.on()` - 主动连接中枢网关或复现 websocket 会话 -- 自动删除旧规则、旧变量或自动清理孤儿变量 - 完整覆盖米家极客版所有节点类型 ## 项目结构 ```text src/ - index.ts # public API - builder.ts # TypeScript 规则 builder - graph.ts # Graph IR 与节点 ID - nodes.ts # 米家节点模板 - compiler.ts # 规则编译与备份合并 - validate.ts # 结构、连接、设备和能力参数校验 - backup.ts # .bak 解包与打包 - cli.ts # 命令入口 -examples/rules/ - index.ts # 最小规则示例 - *.test.ts # Bun 测试 -resources/ - devices.json # 设备清单 - *.bak # 真实备份样本 + index.ts # public API + builder.ts # TypeScript 规则 builder + graph.ts # Graph IR 与节点 ID + nodes.ts # 米家节点模板 + compiler.ts # 规则编译与备份生成 + validate.ts # 结构、连接、设备和能力参数校验 + backup.ts # .bak 解包与打包 + cli.ts # 命令入口(零参数) + devices.json # 设备清单(由 fetch-devices 工具更新) + rules/ + index.ts # 个人规则 + tools/ + fetch-devlist.ts # 中枢网关设备抓取工具 +tests/ + backup.test.ts + compiler.test.ts + validate.test.ts +dist/ # 输出目录(.bak 备份文件) ``` ## 编写规则 @@ -50,7 +52,7 @@ resources/ 规则直接使用 TypeScript: ```ts -import { defineDevices, defineRule } from "../../src/index.ts"; +import { defineDevices, defineRule } from "../index.ts"; const devices = defineDevices({ corridorLight: { @@ -75,24 +77,23 @@ export default defineRule("走廊有人开灯", ({ device, on }) => ## 打包命令 -预览编译结果,不写出文件: +编译规则并生成 `.bak` 备份文件: ```bash -bun run pack -- --rules examples/rules/index.ts --devices resources/devices.json --input "resources/备份2026_4_26 19_17_42.bak" --dry-run +bun run pack ``` -输出新的 `.bak` 文件: +自动读取 `src/rules/index.ts` 和 `src/devices.json`,从零编译,输出到 `dist/miot_{timestamp}.bak`。 + +## 更新设备清单 + +从中枢网关抓取最新设备清单: ```bash -bun run pack -- --rules examples/rules/index.ts --devices resources/devices.json --input "resources/备份2026_4_26 19_17_42.bak" --output dist/miot.bak +bun run fetch-devices 192.168.31.166 048889 ``` -默认策略是追加新规则。显式替换可使用: - -```bash -bun run pack -- --rules examples/rules/index.ts --input old.bak --output new.bak --replace-id 1728570847554 -bun run pack -- --rules examples/rules/index.ts --input old.bak --output new.bak --replace-name 走廊开关灯 -``` +`048889` 是中枢网关屏幕上显示的 6 位 passcode。抓取结果直接写入 `src/devices.json`。 ## 校验策略 @@ -113,7 +114,7 @@ bun run pack -- --rules examples/rules/index.ts --input old.bak --output new.bak bun test ``` -测试覆盖真实备份解包统计、备份打包往返、规则编译输出、备份合并和常见校验错误。 +测试覆盖真实备份解包统计、备份打包往返、规则编译输出和常见校验错误。 ## 导入验证建议 diff --git a/bun.lock b/bun.lock index d199f58..874bb45 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,10 @@ "workspaces": { "": { "name": "miot_x", + "dependencies": { + "bn.js": "^5.2.1", + "elliptic": "^6.6.1", + }, "devDependencies": { "@types/bun": "latest", }, @@ -17,10 +21,28 @@ "@types/node": ["@types/node@25.6.0", "https://registry.npmmirror.com/@types/node/-/node-25.6.0.tgz", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + "bn.js": ["bn.js@5.2.3", "https://registry.npmmirror.com/bn.js/-/bn.js-5.2.3.tgz", {}, "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w=="], + + "brorand": ["brorand@1.1.0", "https://registry.npmmirror.com/brorand/-/brorand-1.1.0.tgz", {}, "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w=="], + "bun-types": ["bun-types@1.3.13", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.13.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + "elliptic": ["elliptic@6.6.1", "https://registry.npmmirror.com/elliptic/-/elliptic-6.6.1.tgz", { "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", "hash.js": "^1.0.0", "hmac-drbg": "^1.0.1", "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g=="], + + "hash.js": ["hash.js@1.1.7", "https://registry.npmmirror.com/hash.js/-/hash.js-1.1.7.tgz", { "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" } }, "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA=="], + + "hmac-drbg": ["hmac-drbg@1.0.1", "https://registry.npmmirror.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg=="], + + "inherits": ["inherits@2.0.4", "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "minimalistic-assert": ["minimalistic-assert@1.0.1", "https://registry.npmmirror.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], + + "minimalistic-crypto-utils": ["minimalistic-crypto-utils@1.0.1", "https://registry.npmmirror.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", {}, "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="], + "typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.19.2", "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "elliptic/bn.js": ["bn.js@4.12.3", "https://registry.npmmirror.com/bn.js/-/bn.js-4.12.3.tgz", {}, "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g=="], } } diff --git a/openspec/specs/fetch-devices-tool/spec.md b/openspec/specs/fetch-devices-tool/spec.md new file mode 100644 index 0000000..8754adb --- /dev/null +++ b/openspec/specs/fetch-devices-tool/spec.md @@ -0,0 +1,22 @@ +# Capability: Fetch Devices Tool + +## Purpose + +Provide a standalone CLI tool to fetch device lists from a Mi Home central hub gateway via ECJPAKE-encrypted WebSocket connection and write the result to `src/devices.json`. + +## Requirements + +### Requirement: 设备清单抓取工具 +系统 SHALL 提供独立的 CLI 工具,通过 ECJPAKE 加密连接中枢网关 WebSocket 接口,调用 `getDevList` API 获取设备清单,并将结果直接写入 `src/devices.json`。 + +#### Scenario: 通过 IP 和 passcode 抓取设备 +- **WHEN** 用户执行 `bun run fetch-devices ` 命令 +- **THEN** 系统 SHALL 连接 `ws:///centrallinkws/`,完成 ECJPAKE 密钥协商和加密通道建立,调用 `getDevList` API,将结果以 `{ fetchedAt, url, count, devList }` 格式写入 `src/devices.json` + +#### Scenario: passcode 格式校验 +- **WHEN** 用户输入的 passcode 不是 6 位数字 +- **THEN** 系统 SHALL 报错并退出,不写入文件 + +#### Scenario: 连接失败处理 +- **WHEN** 网关连接超时或 WebSocket 连接失败 +- **THEN** 系统 SHALL 报错并退出,不写入文件,不修改已有的 `src/devices.json` diff --git a/openspec/specs/typescript-workflow-authoring/spec.md b/openspec/specs/typescript-workflow-authoring/spec.md index 70a1fb2..d4f6655 100644 --- a/openspec/specs/typescript-workflow-authoring/spec.md +++ b/openspec/specs/typescript-workflow-authoring/spec.md @@ -2,7 +2,7 @@ ## Purpose -Allow users to author 米家极客版 (Mi Home Geek Edition) workflow rules using TypeScript files and a builder API, compile them into backup JSON, and package them as `.bak` files — with validation diagnostics and conservative merge semantics. +Allow users to author 米家极客版 (Mi Home Geek Edition) workflow rules using TypeScript files and a builder API, compile them into backup JSON, and package them as `.bak` files — with validation diagnostics. ## Requirements @@ -74,7 +74,7 @@ Allow users to author 米家极客版 (Mi Home Geek Edition) workflow rules usin - **THEN** 每条输出连接 SHALL 使用 `targetNodeId.targetPortName` 字符串格式,并指向存在的节点输入端口 ### Requirement: 备份文件打包输出 -系统 SHALL 使用已知文件外壳将生成的备份 JSON 打包为米家极客版 `.bak` 文件。 +系统 SHALL 使用已知文件外壳将生成的备份 JSON 打包为米家极客版 `.bak` 文件,CLI 不需要任何参数即可完成全流程。 #### Scenario: 备份文件编码 - **WHEN** 系统写出备份文件 @@ -84,28 +84,10 @@ Allow users to author 米家极客版 (Mi Home Geek Edition) workflow rules usin - **WHEN** 系统解包一个已生成的备份文件 - **THEN** 解包得到的 JSON SHALL 与压缩前的编译结果 JSON 一致 -### Requirement: 保守备份合并 -系统 SHALL 默认保留现有备份内容,只新增或显式替换用户选择的工作流规则。 +#### Scenario: 零参数 CLI 编译 +- **WHEN** 用户执行 `bun run pack` 且不传入任何参数 +- **THEN** 系统 SHALL 自动从 `src/rules/index.ts` 加载规则定义,从 `src/devices.json` 加载设备清单,从零编译生成备份,并写入 `dist/miot_{timestamp}.bak` -#### Scenario: 追加生成规则 -- **WHEN** 用户基于现有备份编译新规则且未指定替换策略 -- **THEN** 系统 SHALL 保留所有已有规则,并追加生成的规则 - -#### Scenario: 按显式身份替换规则 -- **WHEN** 用户指定生成规则按 ID 或唯一名称替换已有规则 -- **THEN** 系统 SHALL 只替换匹配规则,并保持无关规则不变 - -### Requirement: 校验诊断 -系统 SHALL 在写出最终 `.bak` 前校验生成的工作流,并为无效结构报告可操作的诊断信息。 - -#### Scenario: 非法节点 ID -- **WHEN** 生成或用户指定的节点 ID 包含 `0-9a-zA-Z` 之外的字符 -- **THEN** 校验 MUST 失败,并给出能定位非法节点的诊断信息 - -#### Scenario: 非法连接目标 -- **WHEN** 生成连接指向不存在的节点或不存在的输入端口 -- **THEN** 校验 MUST 失败,并给出能定位非法连接的诊断信息 - -#### Scenario: 设备资源中不存在的设备 -- **WHEN** 工作流引用的 `did` 不存在于配置的 `devices.json` 资源中 -- **THEN** 校验 MUST 根据所选严格模式失败或警告,且诊断信息 SHALL 包含缺失的 `did` +#### Scenario: 输出文件命名 +- **WHEN** 系统写出备份文件 +- **THEN** 文件名 SHALL 使用 `miot_{yyyyMMdd_HHmmss}.bak` 格式,位于 `dist/` 目录下 diff --git a/package.json b/package.json index 8262213..5fafb23 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,13 @@ "private": true, "scripts": { "pack": "bun run src/cli.ts", + "fetch-devices": "bun run src/tools/fetch-devlist.ts", "test": "bun test" }, + "dependencies": { + "bn.js": "^5.2.1", + "elliptic": "^6.6.1" + }, "devDependencies": { "@types/bun": "latest" }, diff --git a/src/builder.ts b/src/builder.ts index 8fd16e7..e502eba 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -51,7 +51,6 @@ export function defineRule(name: string, factory: RuleFactory, options: RuleOpti name, id: options.id, enable: options.enable ?? true, - replace: options.replace, workflow, }; } diff --git a/src/cli.ts b/src/cli.ts index a21a5af..cca2ef7 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,101 +1,42 @@ import { mkdirSync } from "node:fs"; -import { dirname, resolve } from "node:path"; -import { pathToFileURL } from "node:url"; -import { readBackupFile, readDeviceInventoryFile, writeBackupFile } from "./backup"; +import { resolve } from "node:path"; +import { writeBackupFile } from "./backup"; import { collectRuleDefinitions, compileWorkflow } from "./compiler"; -import type { Diagnostic, RuleDefinition, RuleReplaceStrategy } from "./types"; +import type { Diagnostic } from "./types"; -interface CliOptions { - rules?: string; - input?: string; - output?: string; - devices?: string; - dryRun: boolean; - replace?: RuleReplaceStrategy; -} +const srcDir = import.meta.dir; -export async function runCli(argv = process.argv.slice(2)): Promise { - const options = parseArgs(argv); - if (!options.rules) { - throw new Error("缺少 --rules 规则入口"); - } - - const rulesModule = await import(pathToFileURL(resolve(options.rules)).href) as Record; - const definitions = applyReplaceStrategy(collectRuleDefinitions(rulesModule), options.replace); +async function runCli(): Promise { + const rulesModule = await import(resolve(srcDir, "rules", "index.ts")); + const definitions = collectRuleDefinitions(rulesModule as Record); if (definitions.length === 0) { throw new Error("规则入口没有导出任何 defineRule 结果"); } - const baseBackup = options.input ? readBackupFile(options.input) : undefined; - const devices = options.devices ? readDeviceInventoryFile(options.devices) : undefined; - const result = compileWorkflow(definitions, baseBackup, { devices, deviceStrict: true }); - printSummary(result.summary.addedRules, result.summary.nodeCount, result.summary.deviceDids, result.diagnostics, options); + const devices = (await import("./devices.json")).default; + const result = compileWorkflow(definitions, { devices, deviceStrict: true }); - const errors = result.diagnostics.filter((diagnostic) => diagnostic.severity === "error"); + printSummary(result.summary.addedRules, result.summary.nodeCount, result.summary.deviceDids, result.diagnostics); + + const errors = result.diagnostics.filter((diagnostic: Diagnostic) => diagnostic.severity === "error"); if (errors.length > 0) { throw new Error("编译校验失败,未写出备份文件"); } - if (!options.dryRun) { - if (!options.output) { - throw new Error("缺少 --output 输出备份路径;如只想预览请使用 --dry-run"); - } - mkdirSync(dirname(resolve(options.output)), { recursive: true }); - writeBackupFile(options.output, result.backup); - console.log(`输出备份: ${options.output}`); - } -} - -function parseArgs(argv: string[]): CliOptions { - const options: CliOptions = { dryRun: false }; - for (let index = 0; index < argv.length; index += 1) { - const arg = argv[index]; - if (arg === "--dry-run") { - options.dryRun = true; - continue; - } - if (arg === "--rules") { - options.rules = readArgValue(argv, ++index, arg); - continue; - } - if (arg === "--input") { - options.input = readArgValue(argv, ++index, arg); - continue; - } - if (arg === "--output") { - options.output = readArgValue(argv, ++index, arg); - continue; - } - if (arg === "--devices") { - options.devices = readArgValue(argv, ++index, arg); - continue; - } - if (arg === "--replace-id") { - options.replace = { mode: "replaceById", id: readArgValue(argv, ++index, arg) }; - continue; - } - if (arg === "--replace-name") { - options.replace = { mode: "replaceByName", name: readArgValue(argv, ++index, arg) }; - continue; - } - throw new Error(`未知参数: ${arg}`); - } - return options; -} - -function readArgValue(argv: string[], index: number, name: string): string { - const value = argv[index]; - if (!value) { - throw new Error(`${name} 缺少参数值`); - } - return value; -} - -function applyReplaceStrategy(definitions: RuleDefinition[], replace?: RuleReplaceStrategy): RuleDefinition[] { - if (!replace) { - return definitions; - } - return definitions.map((definition) => ({ ...definition, replace })); + const now = new Date(); + const timestamp = [ + now.getFullYear(), + String(now.getMonth() + 1).padStart(2, "0"), + String(now.getDate()).padStart(2, "0"), + "_", + String(now.getHours()).padStart(2, "0"), + String(now.getMinutes()).padStart(2, "0"), + String(now.getSeconds()).padStart(2, "0"), + ].join(""); + const outputPath = resolve(srcDir, "..", "dist", `miot_${timestamp}.bak`); + mkdirSync(resolve(srcDir, "..", "dist"), { recursive: true }); + writeBackupFile(outputPath, result.backup); + console.log(`输出备份: ${outputPath}`); } function printSummary( @@ -103,15 +44,12 @@ function printSummary( nodeCount: number, deviceDids: string[], diagnostics: Diagnostic[], - options: CliOptions, ): void { console.log("编译摘要:"); console.log(`规则数量: ${ruleNames.length}`); console.log(`规则名称: ${ruleNames.join(", ")}`); console.log(`节点数量: ${nodeCount}`); console.log(`设备引用: ${deviceDids.length > 0 ? deviceDids.join(", ") : "无"}`); - console.log(`输入备份: ${options.input ?? "空备份"}`); - console.log(`输出备份: ${options.dryRun ? "dry-run 未写出" : options.output ?? "未指定"}`); for (const diagnostic of diagnostics) { console.log(`${diagnostic.severity.toUpperCase()} ${diagnostic.code}: ${diagnostic.message}`); } diff --git a/src/compiler.ts b/src/compiler.ts index 1fe0ec0..952f05c 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -21,7 +21,6 @@ import type { CompileOptions, CompileResult, ConditionExpr, - Diagnostic, Graph, GraphNode, RuleDefinition, @@ -60,14 +59,16 @@ export function compileRule(definition: RuleDefinition, options: CompileOptions export function compileWorkflow( definitions: RuleDefinition[], - baseBackup?: BackupData, options: CompileOptions = {}, ): CompileResult { const compiled = compileRules(definitions, options); - const mergeDiagnostics: Diagnostic[] = []; - const backup = mergeCompiledRules(baseBackup, compiled, mergeDiagnostics); - const diagnostics = [...mergeDiagnostics, ...validateBackup(backup, options)]; - const summary = summarizeCompile(compiled, mergeDiagnostics); + const backup: BackupData = { + version: 2, + rules: compiled.map((item) => item.rule), + variables: { global: {} }, + }; + const diagnostics = validateBackup(backup, options); + const summary = summarizeCompile(compiled); return { backup, rules: compiled.map((item) => item.rule), @@ -76,60 +77,6 @@ export function compileWorkflow( }; } -export function mergeCompiledRules( - baseBackup: BackupData | undefined, - compiled: CompiledRule[], - diagnostics: Diagnostic[] = [], -): BackupData { - const next: BackupData = cloneBackup( - baseBackup ?? { - version: 2, - rules: [], - variables: { global: {} }, - }, - ); - - for (const item of compiled) { - const strategy = item.definition.replace ?? { mode: "append" as const }; - if (strategy.mode === "append") { - next.rules.push(item.rule); - continue; - } - - if (strategy.mode === "replaceById") { - const index = next.rules.findIndex((rule) => rule.id === strategy.id); - if (index >= 0) { - next.rules[index] = item.rule; - } else { - diagnostics.push({ - severity: "warning", - code: "replace-id-not-found", - message: `未找到 ID 为 ${strategy.id} 的规则,已改为追加 ${item.definition.name}`, - }); - next.rules.push(item.rule); - } - continue; - } - - const name = strategy.name ?? item.definition.name; - const matches = next.rules - .map((rule, index) => ({ rule, index })) - .filter(({ rule }) => rule.cfg.userData.name === name); - if (matches.length === 1) { - next.rules[matches[0]!.index] = item.rule; - } else { - diagnostics.push({ - severity: "warning", - code: "replace-name-not-unique", - message: `规则名称 ${name} 匹配数量为 ${matches.length},已改为追加 ${item.definition.name}`, - }); - next.rules.push(item.rule); - } - } - - return next; -} - export function collectRuleDefinitions(moduleExports: Record): RuleDefinition[] { const definitions: RuleDefinition[] = []; const seen = new Set(); @@ -310,13 +257,9 @@ function isRuleDefinition(value: unknown): value is RuleDefinition { return typeof value === "object" && value !== null && (value as { kind?: unknown }).kind === "rule-definition"; } -function summarizeCompile(compiled: CompiledRule[], diagnostics: Diagnostic[]) { - const replacedRules = diagnostics - .filter((diagnostic) => diagnostic.code.startsWith("replace-")) - .map((diagnostic) => diagnostic.message); +function summarizeCompile(compiled: CompiledRule[]) { return { addedRules: compiled.map((item) => item.rule.cfg.userData.name), - replacedRules, nodeCount: compiled.reduce((sum, item) => sum + item.rule.nodes.length, 0), deviceDids: collectDeviceDids(compiled), }; @@ -335,10 +278,6 @@ function collectDeviceDids(compiled: CompiledRule[]): string[] { return [...dids].sort(); } -function cloneBackup(backup: BackupData): BackupData { - return cloneJson(backup); -} - function cloneJson(value: T): T { return JSON.parse(JSON.stringify(value)) as T; } diff --git a/src/devices.json b/src/devices.json new file mode 100644 index 0000000..8dd4daf --- /dev/null +++ b/src/devices.json @@ -0,0 +1,1378 @@ +{ + "fetchedAt": "2026-05-06T15:25:09.704Z", + "url": "ws://192.168.31.166/centrallinkws/", + "count": 105, + "devList": { + "85421397": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "落地扇", + "model": "zhimi.fan.sa1", + "modelName": "米家直流变频落地扇", + "urn": "urn:miot-spec-v2:device:fan:0000A005:zhimi-sa1:3", + "roomId": "324001641681", + "roomName": "书房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/740.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=5LovORYLn6W42OoxXAjUVAtxcMY=" + }, + "87384945": { + "specV2Access": true, + "specV3Access": false, + "online": false, + "pushAvailable": false, + "name": "米家多功能网关 ", + "model": "lumi.gateway.v3", + "modelName": "小米多功能网关", + "urn": "urn:miot-spec-v2:device:gateway:0000A019:lumi-v3:1", + "roomId": "324001641692", + "roomName": "其他", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/109.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=BjHt23oIEofen4U8RHhL/LZlDK4=" + }, + "118613920": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": false, + "name": "电饭煲", + "model": "chunmi.cooker.normal2", + "modelName": "米家IH电饭煲", + "urn": "urn:miot-spec-v2:device:cooker:0000A00B:chunmi-normal2:1", + "roomId": "324001641685", + "roomName": "厨房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/257.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=8GXTbf8e6gGrApU5+465wCZJJW0=" + }, + "119374861": { + "specV2Access": true, + "specV3Access": false, + "online": false, + "pushAvailable": false, + "knownIssue": [ + { + "code": 1, + "info": "离线时功能异常" + } + ], + "name": "MIX3充电插座", + "model": "chuangmi.plug.m3", + "modelName": "小米米家智能插座WiFi版", + "urn": "urn:miot-spec-v2:device:outlet:0000A002:chuangmi-m3:1", + "roomId": "324001641681", + "roomName": "书房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/763.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=t0Ixyn2dLzdK6ZDab6VRjvJYlmY=" + }, + "124558665": { + "specV2Access": false, + "specV3Access": false, + "online": true, + "pushAvailable": false, + "name": "主卧空调", + "model": "lumi.acpartner.mcn02", + "modelName": "米家空调伴侣2", + "urn": "urn:miot-spec-v2:device:air-conditioner:0000A004:lumi-mcn02:1", + "roomId": "324001641679", + "roomName": "主卧", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/66078.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=/G3pQk+Af/n2jxnz/MGdN5DiCNA=" + }, + "131152757": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": false, + "name": "桃子灯", + "model": "yeelink.light.color2", + "modelName": "Yeelight LED灯泡(彩光版)", + "urn": "urn:miot-spec-v2:device:light:0000A001:yeelink-color2:1", + "roomId": "324001641679", + "roomName": "主卧", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/436.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=94Xghevtrk5MfOFVPk3lvUwHiEQ=" + }, + "132242258": { + "specV2Access": true, + "specV3Access": false, + "online": false, + "pushAvailable": false, + "name": "Yeelight 彩光灯泡 ", + "model": "yeelink.light.color1", + "modelName": "Yeelight 彩光灯泡", + "urn": "urn:miot-spec-v2:device:light:0000A001:yeelink-color1:1", + "roomId": "324001641692", + "roomName": "其他", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/52.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=IHcO2G4KXGaUPFQxAlrP5KHqo0A=" + }, + "132425868": { + "specV2Access": true, + "specV3Access": false, + "online": false, + "pushAvailable": false, + "name": "灯泡", + "model": "yeelink.light.color1", + "modelName": "Yeelight 彩光灯泡", + "urn": "urn:miot-spec-v2:device:light:0000A001:yeelink-color1:1", + "roomId": "324001641692", + "roomName": "其他", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/52.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=IHcO2G4KXGaUPFQxAlrP5KHqo0A=" + }, + "257571089": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": false, + "name": "空气检测仪", + "model": "cgllc.airmonitor.s1", + "modelName": "青萍空气检测仪", + "urn": "urn:miot-spec-v2:device:air-monitor:0000A008:cgllc-s1:1", + "roomId": "324001641681", + "roomName": "书房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/841.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=vjGYxd+Rf87OYrX46OSrt4D+vWs=" + }, + "332634466": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "网关", + "model": "lumi.gateway.mgl03", + "modelName": "小米智能多模网关", + "urn": "urn:miot-spec-v2:device:gateway:0000A019:lumi-mgl03:2", + "roomId": "324001641681", + "roomName": "书房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/1111.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=Oi/GDBSXQf3x6k/QfpCpBv9CZG8=" + }, + "426597044": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": false, + "name": "魔凡智能即热泡茶机 ", + "model": "morfun.ysj.mf213", + "modelName": "魔凡智能即热泡茶机", + "urn": "urn:miot-spec-v2:device:water-dispenser:0000A0A1:morfun-mf213:1", + "roomId": "324001641692", + "roomName": "其他", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/4698.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=JrVJddIyZB+XBe7yBd5f9LXfnaI=" + }, + "436044621": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": false, + "name": "破壁机", + "model": "chunmi.juicer.a1", + "modelName": "米家智能破壁料理机", + "urn": "urn:miot-spec-v2:device:juicer:0000A04D:chunmi-a1:1", + "roomId": "324001641685", + "roomName": "厨房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/2477.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=dRWCWGgHtnbizUpV09iwfB8R92U=" + }, + "457459710": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": false, + "name": "洗碗机", + "model": "viomi.dishwasher.m02", + "modelName": "米家互联网洗碗机 4套台面式", + "urn": "urn:miot-spec-v2:device:dishwasher:0000A034:viomi-m02:1", + "roomId": "324001654291", + "roomName": "小阳台", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/67150.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=aO/ZoY49cuDZdkVpwc0GSsGUR5I=" + }, + "472775914": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": false, + "name": "电压力锅", + "model": "chunmi.pre_cooker.dylg5", + "modelName": "米家智能电压力锅5L", + "urn": "urn:miot-spec-v2:device:pressure-cooker:0000A04B:chunmi-dylg5:1", + "roomId": "324001641685", + "roomName": "厨房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/3498.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=ZzKpk4A0bmqy7fL5yS6w3IQHOmU=" + }, + "474722409": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": false, + "name": "米家追光氛围灯带 ", + "model": "philips.light.strip3", + "modelName": "米家追光氛围灯带", + "urn": "urn:miot-spec-v2:device:light:0000A001:philips-strip3:2", + "roomId": "324001641681", + "roomName": "书房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/4729.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=A1iwtOyWPXm3cSV3jOS9EJivRb0=" + }, + "619252566": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "三体场景传感器", + "model": "ainice.sensor_occupy.3b", + "modelName": "AInice三体场景传感器", + "urn": "urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:ainice-3b:2", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/18672.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=1JOPyq6TPYZ4LXfZlLw66MtCYWA=" + }, + "622736165": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "米家智能除湿机", + "model": "dmaker.derh.50l", + "modelName": "米家智能除湿机 50L", + "urn": "urn:miot-spec-v2:device:dehumidifier:0000A02D:dmaker-50l:1", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/10908.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=ajbdjadm8Jpb18fveo8bCSAygtc=" + }, + "648840056": { + "specV2Access": true, + "specV3Access": false, + "online": false, + "pushAvailable": false, + "name": "小米八电极体脂秤 ", + "model": "yunmai.scales.ms3001", + "modelName": "小米八电极体脂秤", + "urn": "urn:miot-spec-v2:device:scale:0000A07D:yunmai-ms3001:1", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/9710.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=o81lZneDglEL7FFQnxk4+qnNQ2o=" + }, + "705981089": { + "specV2Access": false, + "specV3Access": false, + "online": true, + "pushAvailable": false, + "name": "书房空调", + "model": "lumi.acpartner.mcn02", + "modelName": "米家空调伴侣2", + "urn": "urn:miot-spec-v2:device:air-conditioner:0000A004:lumi-mcn02:1", + "roomId": "324001641681", + "roomName": "书房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/66078.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=/G3pQk+Af/n2jxnz/MGdN5DiCNA=" + }, + "707061212": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "净水器", + "model": "xiaomi.waterpuri.q1000", + "modelName": "米家即热净水器Q1000", + "urn": "urn:miot-spec-v2:device:water-purifier:0000A013:xiaomi-q1000:2", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/13194.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=HFRVlsKq2KvdGDxf4iENS2phlqQ=" + }, + "710567216": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "小米智能家庭面板 ", + "model": "xiaomi.controller.86v1", + "modelName": "小米智能家庭面板", + "urn": "urn:miot-spec-v2:device:control-panel:0000A099:xiaomi-86v1:1", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/9955.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=B7O+0WBeOFjcEJdz+lnKZillf8I=" + }, + "725742590": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "书房的小爱音箱", + "model": "xiaomi.wifispeaker.l06a", + "modelName": "小爱音箱", + "urn": "urn:miot-spec-v2:device:speaker:0000A015:xiaomi-l06a:2", + "roomId": "324001641681", + "roomName": "书房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/1496.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=omI/v/TxlNqu/IBXE6n8HELLuJM=" + }, + "725743256": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "客厅的小爱音箱", + "model": "xiaomi.wifispeaker.l06a", + "modelName": "小爱音箱", + "urn": "urn:miot-spec-v2:device:speaker:0000A015:xiaomi-l06a:2", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/1496.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=omI/v/TxlNqu/IBXE6n8HELLuJM=" + }, + "731514866": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "米家智能微波炉", + "model": "chunmi.microwave.cmwb3", + "modelName": "米家智能微波炉 20L", + "urn": "urn:miot-spec-v2:device:microwave-oven:0000A032:chunmi-cmwb3:1", + "roomId": "324001641685", + "roomName": "厨房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/14931.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=urh/M0/CHFlpQ7J0jjZnvJKRJck=" + }, + "735861221": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "空气净化器", + "model": "xiaomi.airp.ua3", + "modelName": "米家全效空气净化器 Ultra 增强版", + "urn": "urn:miot-spec-v2:device:air-purifier:0000A007:xiaomi-ua3:2", + "roomId": "324001641679", + "roomName": "主卧", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/16990.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=FjprAc9CItKqeOdcIwUFCA6ZRZ4=" + }, + "746603274": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": false, + "name": "米家电风扇", + "model": "dmaker.fan.p28", + "modelName": "米家智能直流变频循环扇 落地式", + "urn": "urn:miot-spec-v2:device:fan:0000A005:dmaker-p28:1", + "roomId": "324001641679", + "roomName": "主卧", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/7212.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=FtoHauXMP1w4/D6W99yRhGn9T3I=" + }, + "759639338": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "主卧的小爱音箱", + "model": "xiaomi.wifispeaker.l06a", + "modelName": "小爱音箱", + "urn": "urn:miot-spec-v2:device:speaker:0000A015:xiaomi-l06a:2", + "roomId": "324001641679", + "roomName": "主卧", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/1496.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=omI/v/TxlNqu/IBXE6n8HELLuJM=" + }, + "771695338": { + "specV2Access": true, + "specV3Access": false, + "online": false, + "pushAvailable": false, + "name": "rd15_minet_ae13", + "model": "xiaomi.router.rd15", + "modelName": "Xiaomi路由器BE3600 2.5G版", + "urn": "urn:miot-spec-v2:device:router:0000A036:xiaomi-rd15:1", + "roomId": "324001641681", + "roomName": "书房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/79936.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=89+E95yWfyojWt2GSsUcgn+f/9w=" + }, + "818256564": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": false, + "name": "主路由器", + "model": "xiaomi.router.rd18", + "modelName": "Xiaomi路由器BE5000", + "urn": "urn:miot-spec-v2:device:router:0000A036:xiaomi-rd18:1", + "roomId": "324001641694", + "roomName": "玄关", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/79946.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=Ebkeo6A+GnvBcrbLoUmhWA7BGhw=" + }, + "819627147": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "空调", + "model": "xiaomi.airc.rr1r00", + "modelName": "空调 自然风Pro 双出风 立式3匹 超一级能效", + "urn": "urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-rr1r00:3", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/18806.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=M9BKMhNXWqjaEsBjVxBko9b1bhY=" + }, + "820730869": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "小米人在传感器", + "model": "xiaomi.sensor_occupy.p1", + "modelName": "小米人在传感器Pro", + "urn": "urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:xiaomi-p1:2", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/19390.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=hzC8gM9NeDgMfl+IXoa6aNs/JxU=" + }, + "824826308": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": false, + "name": "吸顶灯", + "model": "yeelink.light.ceiling22", + "modelName": "米家卧室吸顶灯450", + "urn": "urn:miot-spec-v2:device:light:0000A001:yeelink-ceiling22:2", + "roomId": "324001641681", + "roomName": "书房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/1295.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=dJiMeyCsMDuiRgY5ryTxV1R5/F0=" + }, + "824827669": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": false, + "name": "吸顶灯", + "model": "yeelink.light.ceiling22", + "modelName": "米家卧室吸顶灯450", + "urn": "urn:miot-spec-v2:device:light:0000A001:yeelink-ceiling22:2", + "roomId": "324001641679", + "roomName": "主卧", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/1295.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=dJiMeyCsMDuiRgY5ryTxV1R5/F0=" + }, + "824910954": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "米家风扇灯", + "model": "xiaomi.light.cfan", + "modelName": "米家风扇灯 42英寸", + "urn": "urn:miot-spec-v2:device:light:0000A001:xiaomi-cfan:1:0000C802", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/16391.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=yX4gAUDsz+nsCK6DahDaoaUQev4=" + }, + "965557344": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "米家智能电蒸锅", + "model": "xiaomi.esteamer.mes01", + "modelName": "米家智能电蒸锅 12L", + "urn": "urn:miot-spec-v2:device:electric-steamer:0000A0C9:xiaomi-mes01:1", + "roomId": "324001654291", + "roomName": "小阳台", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/13231.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=us1Up7wkuoh/a/caR6Ql6gZRfT8=" + }, + "1063785107": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "阳台开关", + "model": "zimi.switch.dhkg02", + "modelName": "小米米家智能开关(双开单控)", + "urn": "urn:miot-spec-v2:device:switch:0000A003:zimi-dhkg02:2:0000C809", + "roomId": "324001641683", + "roomName": "阳台", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/1946.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=AYVrbaWtO3fjTCaNpzTpsthqoGA=" + }, + "1065517287": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "走廊开关-中", + "model": "zimi.switch.dhkg02", + "modelName": "小米米家智能开关(双开单控)", + "urn": "urn:miot-spec-v2:device:switch:0000A003:zimi-dhkg02:2:0000C809", + "roomId": "324001641691", + "roomName": "走廊", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/1946.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=AYVrbaWtO3fjTCaNpzTpsthqoGA=" + }, + "1065584597": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "门口开关", + "model": "zimi.switch.dhkg02", + "modelName": "小米米家智能开关(双开单控)", + "urn": "urn:miot-spec-v2:device:switch:0000A003:zimi-dhkg02:2:0000C809", + "roomId": "324001641681", + "roomName": "书房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/1946.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=AYVrbaWtO3fjTCaNpzTpsthqoGA=" + }, + "1065586768": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "门口开关", + "model": "zimi.switch.dhkg02", + "modelName": "小米米家智能开关(双开单控)", + "urn": "urn:miot-spec-v2:device:switch:0000A003:zimi-dhkg02:2:0000C809", + "roomId": "324001641679", + "roomName": "主卧", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/1946.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=AYVrbaWtO3fjTCaNpzTpsthqoGA=" + }, + "1069604786": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "小米智能门锁", + "model": "loock.lock.r2", + "modelName": "小米智能门锁 M20 Pro", + "urn": "urn:miot-spec-v2:device:lock:0000A038:loock-r2:1", + "roomId": "324001641694", + "roomName": "玄关", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/12183.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=oTsuThhQUNTpvk9+qCHrOvl+mQs=" + }, + "1080556715": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "鞋柜插座", + "model": "zimi.plug.zncz01", + "modelName": "小米米家智能墙壁插座", + "urn": "urn:miot-spec-v2:device:outlet:0000A002:zimi-zncz01:2:0000C816", + "roomId": "324001641694", + "roomName": "玄关", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/3083.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=lDr7/YDYj9/lAfIjmqW4X6SrSAA=" + }, + "1088897819": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "轨道灯-泛光灯", + "model": "lemesh.light.wy0c15", + "modelName": "情景Mesh色温灯V2S系列", + "urn": "urn:miot-spec-v2:device:light:0000A001:lemesh-wy0c15:1:0000C802", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/13525.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=yDGqK5X5pQnEmyDoT9M/sNnBGqs=" + }, + "1088897821": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "轨道灯-泛光灯", + "model": "lemesh.light.wy0c15", + "modelName": "情景Mesh色温灯V2S系列", + "urn": "urn:miot-spec-v2:device:light:0000A001:lemesh-wy0c15:1:0000C802", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/13525.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=yDGqK5X5pQnEmyDoT9M/sNnBGqs=" + }, + "1098680938": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "轨道灯-柜子射灯", + "model": "lemesh.light.wy0c15", + "modelName": "情景Mesh色温灯V2S系列", + "urn": "urn:miot-spec-v2:device:light:0000A001:lemesh-wy0c15:1:0000C802", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/13525.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=yDGqK5X5pQnEmyDoT9M/sNnBGqs=" + }, + "1098681281": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "轨道灯-射灯", + "model": "lemesh.light.wy0c15", + "modelName": "情景Mesh色温灯V2S系列", + "urn": "urn:miot-spec-v2:device:light:0000A001:lemesh-wy0c15:1:0000C802", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/13525.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=yDGqK5X5pQnEmyDoT9M/sNnBGqs=" + }, + "1098681948": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "轨道灯-餐桌射灯", + "model": "lemesh.light.wy0c15", + "modelName": "情景Mesh色温灯V2S系列", + "urn": "urn:miot-spec-v2:device:light:0000A001:lemesh-wy0c15:1:0000C802", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/13525.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=yDGqK5X5pQnEmyDoT9M/sNnBGqs=" + }, + "1098683357": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "轨道灯-玄关射灯", + "model": "lemesh.light.wy0c15", + "modelName": "情景Mesh色温灯V2S系列", + "urn": "urn:miot-spec-v2:device:light:0000A001:lemesh-wy0c15:1:0000C802", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/13525.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=yDGqK5X5pQnEmyDoT9M/sNnBGqs=" + }, + "1099107996": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "门口开关-右", + "model": "zimi.switch.dhkg05", + "modelName": "小米米家智能开关(三开单控)", + "urn": "urn:miot-spec-v2:device:switch:0000A003:zimi-dhkg05:1:0000C810", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/5937.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=DbcjNVKZELzjZxCpsjzBAklM7ko=" + }, + "1099173645": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "门口开关-中", + "model": "zimi.switch.dhkg05", + "modelName": "小米米家智能开关(三开单控)", + "urn": "urn:miot-spec-v2:device:switch:0000A003:zimi-dhkg05:1:0000C810", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/5937.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=DbcjNVKZELzjZxCpsjzBAklM7ko=" + }, + "1100219470": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "灯带", + "model": "lemesh.light.wy0c15", + "modelName": "情景Mesh色温灯V2S系列", + "urn": "urn:miot-spec-v2:device:light:0000A001:lemesh-wy0c15:1:0000C802", + "roomId": "324001641679", + "roomName": "主卧", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/13525.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=yDGqK5X5pQnEmyDoT9M/sNnBGqs=" + }, + "1100219493": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "灯带", + "model": "lemesh.light.wy0c15", + "modelName": "情景Mesh色温灯V2S系列", + "urn": "urn:miot-spec-v2:device:light:0000A001:lemesh-wy0c15:1:0000C802", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/13525.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=yDGqK5X5pQnEmyDoT9M/sNnBGqs=" + }, + "1104758822": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "中枢网关", + "model": "xiaomi.gateway.hub1", + "modelName": "Xiaomi 中枢网关", + "urn": "urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:3", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/6482.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=aWKBwOP0ibVdpYJkfpTRmlJ5Ho0=" + }, + "1104762935": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "小米智能摄像机", + "model": "chuangmi.camera.81ac1", + "modelName": "小米智能摄像机C700", + "urn": "urn:miot-spec-v2:device:camera:0000A01C:chuangmi-81ac1:4", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/18691.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=GEkUU3sGFVXXtt/kIDf86MG16YQ=" + }, + "1115616869": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "厨房筒灯-左", + "model": "mvs.light.wy0a01", + "modelName": "智能筒射灯Mesh版", + "urn": "urn:miot-spec-v2:device:light:0000A001:mvs-wy0a01:1:0000C802", + "roomId": "324001641685", + "roomName": "厨房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/17964.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=QfTfmXEkm6NEdR/M6J0mdi+3myg=" + }, + "1115654769": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "玄关筒灯", + "model": "lemesh.light.wy0c15", + "modelName": "情景Mesh色温灯V2S系列", + "urn": "urn:miot-spec-v2:device:light:0000A001:lemesh-wy0c15:1:0000C802", + "roomId": "324001641694", + "roomName": "玄关", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/13525.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=yDGqK5X5pQnEmyDoT9M/sNnBGqs=" + }, + "1115656394": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "筒灯", + "model": "lemesh.light.wy0c15", + "modelName": "情景Mesh色温灯V2S系列", + "urn": "urn:miot-spec-v2:device:light:0000A001:lemesh-wy0c15:1:0000C802", + "roomId": "324001654291", + "roomName": "小阳台", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/13525.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=yDGqK5X5pQnEmyDoT9M/sNnBGqs=" + }, + "1116837766": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "筒灯-左", + "model": "mvs.light.wy0a01", + "modelName": "智能筒射灯Mesh版", + "urn": "urn:miot-spec-v2:device:light:0000A001:mvs-wy0a01:1:0000C802", + "roomId": "324001641683", + "roomName": "阳台", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/17964.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=QfTfmXEkm6NEdR/M6J0mdi+3myg=" + }, + "1117015418": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "客厅射灯-西北", + "model": "mvs.light.wy0a01", + "modelName": "智能筒射灯Mesh版", + "urn": "urn:miot-spec-v2:device:light:0000A001:mvs-wy0a01:1:0000C802", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/17964.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=QfTfmXEkm6NEdR/M6J0mdi+3myg=" + }, + "1117068239": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "走廊射灯-东", + "model": "mvs.light.wy0a01", + "modelName": "智能筒射灯Mesh版", + "urn": "urn:miot-spec-v2:device:light:0000A001:mvs-wy0a01:1:0000C802", + "roomId": "324001641691", + "roomName": "走廊", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/17964.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=QfTfmXEkm6NEdR/M6J0mdi+3myg=" + }, + "1117071331": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "走廊射灯-西", + "model": "mvs.light.wy0a01", + "modelName": "智能筒射灯Mesh版", + "urn": "urn:miot-spec-v2:device:light:0000A001:mvs-wy0a01:1:0000C802", + "roomId": "324001641691", + "roomName": "走廊", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/17964.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=QfTfmXEkm6NEdR/M6J0mdi+3myg=" + }, + "1117072505": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "厕所灯-左", + "model": "mvs.light.wy0a01", + "modelName": "智能筒射灯Mesh版", + "urn": "urn:miot-spec-v2:device:light:0000A001:mvs-wy0a01:1:0000C802", + "roomId": "324001641682", + "roomName": "厕所", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/17964.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=QfTfmXEkm6NEdR/M6J0mdi+3myg=" + }, + "1117074356": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "厨房筒灯-右", + "model": "mvs.light.wy0a01", + "modelName": "智能筒射灯Mesh版", + "urn": "urn:miot-spec-v2:device:light:0000A001:mvs-wy0a01:1:0000C802", + "roomId": "324001641685", + "roomName": "厨房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/17964.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=QfTfmXEkm6NEdR/M6J0mdi+3myg=" + }, + "1117075017": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "厕所灯-右", + "model": "mvs.light.wy0a01", + "modelName": "智能筒射灯Mesh版", + "urn": "urn:miot-spec-v2:device:light:0000A001:mvs-wy0a01:1:0000C802", + "roomId": "324001641682", + "roomName": "厕所", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/17964.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=QfTfmXEkm6NEdR/M6J0mdi+3myg=" + }, + "1117076659": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "筒灯-中", + "model": "mvs.light.wy0a01", + "modelName": "智能筒射灯Mesh版", + "urn": "urn:miot-spec-v2:device:light:0000A001:mvs-wy0a01:1:0000C802", + "roomId": "324001641683", + "roomName": "阳台", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/17964.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=QfTfmXEkm6NEdR/M6J0mdi+3myg=" + }, + "1118066712": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "客厅射灯-东北", + "model": "mvs.light.wy0a01", + "modelName": "智能筒射灯Mesh版", + "urn": "urn:miot-spec-v2:device:light:0000A001:mvs-wy0a01:1:0000C802", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/17964.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=QfTfmXEkm6NEdR/M6J0mdi+3myg=" + }, + "1118105268": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "客厅射灯-西南", + "model": "mvs.light.wy0a01", + "modelName": "智能筒射灯Mesh版", + "urn": "urn:miot-spec-v2:device:light:0000A001:mvs-wy0a01:1:0000C802", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/17964.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=QfTfmXEkm6NEdR/M6J0mdi+3myg=" + }, + "1118105541": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "阳台-右", + "model": "mvs.light.wy0a01", + "modelName": "智能筒射灯Mesh版", + "urn": "urn:miot-spec-v2:device:light:0000A001:mvs-wy0a01:1:0000C802", + "roomId": "324001641683", + "roomName": "阳台", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/17964.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=QfTfmXEkm6NEdR/M6J0mdi+3myg=" + }, + "1118108714": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "客厅射灯-东南", + "model": "mvs.light.wy0a01", + "modelName": "智能筒射灯Mesh版", + "urn": "urn:miot-spec-v2:device:light:0000A001:mvs-wy0a01:1:0000C802", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/17964.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=QfTfmXEkm6NEdR/M6J0mdi+3myg=" + }, + "1120255507": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "柜子射灯", + "model": "lemesh.light.wy0c24", + "modelName": "情景Mesh色温灯V2S Pro", + "urn": "urn:miot-spec-v2:device:light:0000A001:lemesh-wy0c24:1:0000C802", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/17687.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=YgpaZQ5EWXxG2zeazhyexi8VHFo=" + }, + "1121159625": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "冰箱射灯", + "model": "lemesh.light.wy0c24", + "modelName": "情景Mesh色温灯V2S Pro", + "urn": "urn:miot-spec-v2:device:light:0000A001:lemesh-wy0c24:1:0000C802", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/17687.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=YgpaZQ5EWXxG2zeazhyexi8VHFo=" + }, + "1156242458": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "窗帘", + "model": "lumi.curtain.hmcn02", + "modelName": "米家窗帘伴侣", + "urn": "urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn02:1:0000C817", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/3129.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=PcEwaQCtfNbRF4N7DPMXAPiTPII=" + }, + "1165621862": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "窗帘", + "model": "lumi.curtain.hmcn02", + "modelName": "米家窗帘伴侣", + "urn": "urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn02:1:0000C817", + "roomId": "324001641679", + "roomName": "主卧", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/3129.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=PcEwaQCtfNbRF4N7DPMXAPiTPII=" + }, + "blt.3.1adpj6r3gls00": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "橙子脚下夜灯", + "model": "yeelink.light.nl1", + "modelName": "米家夜灯2 蓝牙版", + "urn": "urn:miot-spec-v2:device:night-light:0000A0AB:yeelink-nl1:1", + "roomId": "324001641679", + "roomName": "主卧", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/2038.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=rx6UQWUW3pP9Rl018vnIu5FPq0U=" + }, + "blt.3.1b9p8h6sg5k00": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "温湿度计", + "model": "miaomiaoce.sensor_ht.t6", + "modelName": "小米电子温湿度计", + "urn": "urn:miot-spec-v2:device:temperature-humidity-sensor:0000A00A:miaomiaoce-t6:1", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/4611.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=SKeTi88u7PggOpEbziLB920t2ZE=" + }, + "blt.3.1b9pnuk8k5k00": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "温湿度计", + "model": "miaomiaoce.sensor_ht.t6", + "modelName": "小米电子温湿度计", + "urn": "urn:miot-spec-v2:device:temperature-humidity-sensor:0000A00A:miaomiaoce-t6:1", + "roomId": "324001641694", + "roomName": "玄关", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/4611.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=SKeTi88u7PggOpEbziLB920t2ZE=" + }, + "blt.3.1htip66f4co00": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "户外光线感应", + "model": "xiaomi.motion.pir1", + "modelName": "小米人体传感器2S", + "urn": "urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1:2", + "roomId": "324001654291", + "roomName": "小阳台", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/13617.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=wZFZai4XC8lkUHlC1W4LNU/DQqk=" + }, + "blt.3.1htiptpdgco00": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "人体传感器", + "model": "xiaomi.motion.pir1", + "modelName": "小米人体传感器2S", + "urn": "urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1:2", + "roomId": "324001641691", + "roomName": "走廊", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/13617.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=wZFZai4XC8lkUHlC1W4LNU/DQqk=" + }, + "blt.3.1iccji894kc00": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "床头开关", + "model": "yeelink.remote.contrl", + "modelName": "小米无线开关(双键版)", + "urn": "urn:miot-spec-v2:device:remote-control:0000A021:yeelink-contrl:1", + "roomId": "324001641679", + "roomName": "主卧", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/6473.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=eKLu9kzgfzCnKfPjSSJBpKo/B5Y=" + }, + "blt.3.1iccrkjvgkc00": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "门口开关-左", + "model": "yeelink.remote.contrl", + "modelName": "小米无线开关(双键版)", + "urn": "urn:miot-spec-v2:device:remote-control:0000A021:yeelink-contrl:1", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/6473.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=eKLu9kzgfzCnKfPjSSJBpKo/B5Y=" + }, + "blt.3.1iccsgp80kg00": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "书桌开关", + "model": "yeelink.remote.contrl", + "modelName": "小米无线开关(双键版)", + "urn": "urn:miot-spec-v2:device:remote-control:0000A021:yeelink-contrl:1", + "roomId": "324001641681", + "roomName": "书房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/6473.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=eKLu9kzgfzCnKfPjSSJBpKo/B5Y=" + }, + "blt.3.1idl1i2jgk400": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "饮水机水浸传感器", + "model": "lumi.flood.bmcn01", + "modelName": "小米水浸卫士", + "urn": "urn:miot-spec-v2:device:submersion-sensor:0000A024:lumi-bmcn01:2", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/2147.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=tpn79vYNzBvnbh0tyqMw5bcJr0c=" + }, + "blt.3.1idl2um68kc00": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "厨房水槽水浸传感器", + "model": "lumi.flood.bmcn01", + "modelName": "小米水浸卫士", + "urn": "urn:miot-spec-v2:device:submersion-sensor:0000A024:lumi-bmcn01:2", + "roomId": "324001641685", + "roomName": "厨房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/2147.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=tpn79vYNzBvnbh0tyqMw5bcJr0c=" + }, + "blt.3.1idl9cllck800": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "净水器进水口水浸传感器(沙发)", + "model": "lumi.flood.bmcn01", + "modelName": "小米水浸卫士", + "urn": "urn:miot-spec-v2:device:submersion-sensor:0000A024:lumi-bmcn01:2", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/2147.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=tpn79vYNzBvnbh0tyqMw5bcJr0c=" + }, + "blt.3.1imup6vn4kk00": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "小阳台门", + "model": "isa.magnet.dw2hl", + "modelName": "小米门窗传感器2", + "urn": "urn:miot-spec-v2:device:magnet-sensor:0000A016:isa-dw2hl:1", + "roomId": "324001654291", + "roomName": "小阳台", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/2443.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=KTYncSCFyHKTxf387o6fF9wsMbw=" + }, + "blt.3.1inbsmidgkc00": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "温湿度传感器", + "model": "miaomiaoce.sensor_ht.t1", + "modelName": "小米米家电子温湿度计Pro", + "urn": "urn:miot-spec-v2:device:temperature-humidity-sensor:0000A00A:miaomiaoce-t1:1", + "roomId": "324001641682", + "roomName": "厕所", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/1115.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=vpdOha/w5jjfEvlNnqTLz4DJB1U=" + }, + "blt.3.1isf9t1h0k800": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "桃子脚下夜灯", + "model": "yeelink.light.nl1", + "modelName": "米家夜灯2 蓝牙版", + "urn": "urn:miot-spec-v2:device:night-light:0000A0AB:yeelink-nl1:1", + "roomId": "324001641679", + "roomName": "主卧", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/2038.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=rx6UQWUW3pP9Rl018vnIu5FPq0U=" + }, + "blt.3.1isffk884kk00": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "人在传感器", + "model": "xiaomi.sensor_occupy.03", + "modelName": "小米人在传感器", + "urn": "urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:xiaomi-03:3", + "roomId": "324001641682", + "roomName": "厕所", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/18051.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=tB0z4TOxS0mG86wRbxnHWvi96sY=" + }, + "blt.3.1j060afuskk00": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "人在传感器", + "model": "xiaomi.sensor_occupy.03", + "modelName": "小米人在传感器", + "urn": "urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:xiaomi-03:3", + "roomId": "324001641685", + "roomName": "厨房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/18051.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=tB0z4TOxS0mG86wRbxnHWvi96sY=" + }, + "blt.3.1j7s7ogp8kc00": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "人在传感器", + "model": "xiaomi.sensor_occupy.03", + "modelName": "小米人在传感器", + "urn": "urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:xiaomi-03:2", + "roomId": "324001641683", + "roomName": "阳台", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/18051.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=tB0z4TOxS0mG86wRbxnHWvi96sY=" + }, + "blt.3.1jb59sce4kk00": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "人体传感器", + "model": "xiaomi.motion.pir1", + "modelName": "小米人体传感器2S", + "urn": "urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1:2", + "roomId": "324001641683", + "roomName": "阳台", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/13617.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=wZFZai4XC8lkUHlC1W4LNU/DQqk=" + }, + "blt.3.1k3ect1ogkc00": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "温湿度计", + "model": "miaomiaoce.sensor_ht.t2", + "modelName": "小米米家蓝牙温湿度计2", + "urn": "urn:miot-spec-v2:device:temperature-humidity-sensor:0000A00A:miaomiaoce-t2:1", + "roomId": "324001654291", + "roomName": "小阳台", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/1371.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=C/7eH8DFXtwbGQHA4O1A9ZDs6Pk=" + }, + "blt.3.1ksr342j8kk00": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "压力传感器", + "model": "linp.senpres.ps1bb", + "modelName": "领普压力有无传感器", + "urn": "urn:miot-spec-v2:device:pressure-sensor:0000A0D3:linp-ps1bb:1", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/16204.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=P5oyMtgDZ+mQ2ORlya7J1iHkq2A=" + }, + "blt.3.1lraonbe84g01": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "米家智能腰部按摩仪 ", + "model": "xiaomi.magic_touch.yb01", + "modelName": "米家智能腰部按摩仪", + "urn": "urn:miot-spec-v2:device:massager:0000A083:xiaomi-yb01:1", + "roomId": "324001641692", + "roomName": "其他", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/18118.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=Yz8uQlhPknJRx6O+kwz4NWFwTTs=" + }, + "blt.3.1mss7c6ed0g00": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "人体存在传感器", + "model": "linp.sensor_occupy.es2", + "modelName": "领普人体存在传感器ES3", + "urn": "urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:linp-es2:1", + "roomId": "324001641682", + "roomName": "厕所", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/20731.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=UeOKPOVrx486pemWrriZIXAuSyM=" + }, + "group.1815366507212455936": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "客厅射灯", + "model": "mijia.light.group3", + "modelName": "智能灯组", + "urn": "urn:miot-spec-v2:device:light:0000A001:mijia-group3:3:0000C802", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/68286.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=ZwGFM4pyCV9vSXGIvoL33RhEyhE=" + }, + "group.1815368043565035520": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "阳台筒灯", + "model": "mijia.light.group3", + "modelName": "智能灯组", + "urn": "urn:miot-spec-v2:device:light:0000A001:mijia-group3:3:0000C802", + "roomId": "324001641683", + "roomName": "阳台", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/68286.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=ZwGFM4pyCV9vSXGIvoL33RhEyhE=" + }, + "group.1815373077765824512": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "走廊筒灯", + "model": "mijia.light.group3", + "modelName": "智能灯组", + "urn": "urn:miot-spec-v2:device:light:0000A001:mijia-group3:3:0000C802", + "roomId": "324001641691", + "roomName": "走廊", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/68286.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=ZwGFM4pyCV9vSXGIvoL33RhEyhE=" + }, + "group.1815374216385142786": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "厕所筒灯", + "model": "mijia.light.group3", + "modelName": "智能灯组", + "urn": "urn:miot-spec-v2:device:light:0000A001:mijia-group3:3:0000C802", + "roomId": "324001641682", + "roomName": "厕所", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/68286.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=ZwGFM4pyCV9vSXGIvoL33RhEyhE=" + }, + "group.1815378222608162816": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "餐厅灯", + "model": "mijia.light.group3", + "modelName": "智能灯组", + "urn": "urn:miot-spec-v2:device:light:0000A001:mijia-group3:3:0000C802", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/68286.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=ZwGFM4pyCV9vSXGIvoL33RhEyhE=" + }, + "group.1844385587290320896": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "厨房筒灯", + "model": "mijia.light.group3", + "modelName": "智能灯组", + "urn": "urn:miot-spec-v2:device:light:0000A001:mijia-group3:3:0000C802", + "roomId": "324001641685", + "roomName": "厨房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/68286.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=ZwGFM4pyCV9vSXGIvoL33RhEyhE=" + }, + "group.1845094127353090048": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "餐厅射灯", + "model": "mijia.light.group3", + "modelName": "智能灯组", + "urn": "urn:miot-spec-v2:device:light:0000A001:mijia-group3:3:0000C802", + "roomId": "324001641677", + "roomName": "客厅", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/68286.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=ZwGFM4pyCV9vSXGIvoL33RhEyhE=" + }, + "lumi.158d000242334b": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "门窗传感器", + "model": "lumi.sensor_magnet.v2", + "modelName": "小米门窗传感器", + "urn": "urn:miot-spec-v2:device:magnet-sensor:0000A016:lumi-v2:1", + "roomId": "324001641682", + "roomName": "厕所", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/64.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=nrFLIKufEJ/LpZNbX0IJaRq+bDI=" + }, + "lumi.158d000322e2aa": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "展示屏", + "model": "lumi.plug.v1", + "modelName": "米家智能插座(Zigbee版)", + "urn": "urn:miot-spec-v2:device:outlet:0000A002:lumi-v1:1", + "roomId": "324001641681", + "roomName": "书房", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/136.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=vZ9hjNdr4XNjOCRpUnrVcszjzZM=" + }, + "lumi.158d000446c7ee": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "人体传感器", + "model": "lumi.sensor_motion.v2", + "modelName": "小米人体传感器", + "urn": "urn:miot-spec-v2:device:motion-sensor:0000A014:lumi-v2:2", + "roomId": "324001641692", + "roomName": "其他", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/63.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=L/Y4sk+CCzSimAaWM2JO1PXPDWA=" + }, + "lumi.158d00044feffd": { + "specV2Access": true, + "specV3Access": false, + "online": true, + "pushAvailable": true, + "name": "人体传感器", + "model": "lumi.sensor_motion.v2", + "modelName": "小米人体传感器", + "urn": "urn:miot-spec-v2:device:motion-sensor:0000A014:lumi-v2:2", + "roomId": "324001641682", + "roomName": "厕所", + "icon": "https://cnbj1.fds.api.xiaomi.com/iotweb-product-center/63.png?GalaxyAccessKeyId=AKVGLQWBOVIRQ3XLEW&Expires=9223372036854775807&Signature=L/Y4sk+CCzSimAaWM2JO1PXPDWA=" + } + } +} diff --git a/src/index.ts b/src/index.ts index 3918009..1715bd0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,8 +12,8 @@ export { any, not, } from "./builder"; -export { BACKUP_HEADER, backupStats, packBackup, readBackupFile, readDeviceInventoryFile, unpackBackup, writeBackupFile } from "./backup"; -export { collectRuleDefinitions, compileRule, compileRules, compileWorkflow, mergeCompiledRules } from "./compiler"; +export { BACKUP_HEADER, backupStats, packBackup, readBackupFile, unpackBackup, writeBackupFile } from "./backup"; +export { collectRuleDefinitions, compileRule, compileRules, compileWorkflow } from "./compiler"; export { assertValidBackup, validateBackup } from "./validate"; export type { ActionExpr, diff --git a/examples/rules/index.ts b/src/rules/index.ts similarity index 96% rename from examples/rules/index.ts rename to src/rules/index.ts index 2c7041f..b103613 100644 --- a/examples/rules/index.ts +++ b/src/rules/index.ts @@ -1,4 +1,4 @@ -import { defineDevices, defineRule } from "../../src/index.ts"; +import { defineDevices, defineRule } from "../index.ts"; const devices = defineDevices({ corridorLight: { diff --git a/src/tools/fetch-devlist.ts b/src/tools/fetch-devlist.ts new file mode 100644 index 0000000..bac38cb --- /dev/null +++ b/src/tools/fetch-devlist.ts @@ -0,0 +1,620 @@ +import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto"; +import { deflateRawSync, inflateRawSync } from "node:zlib"; +import { writeFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import BN from "bn.js"; +import { ec as EllipticEC } from "elliptic"; + +const DATA_TYPE = { + PROTOCOL_LIST: 1, + SELECTED_PROTOCOL: 2, + SESSION_KEY_EXCHANGE: 3, + ERROR: 4, + DATA: 5, + ECJPAKE_ROUND_ONE: 32, + ECJPAKE_ROUND_TWO: 33, +} as const; + +function usage(exitCode = 0) { + const script = "bun run fetch-devices"; + const text = [ + `Usage: ${script} `, + "", + "Examples:", + ` ${script} 192.168.31.166 048889`, + "", + "Notes:", + " - The passcode is read only from argv and is not written to disk.", + " - Output is written to src/devices.json.", + ].join("\n"); + (exitCode === 0 ? console.log : console.error)(text); + process.exit(exitCode); +} + +function normalizeUrl(input: string): string { + let url: URL; + if (/^wss?:\/\//i.test(input)) { + url = new URL(input); + } else if (/^https?:\/\//i.test(input)) { + url = new URL(input); + url.protocol = url.protocol === "https:" ? "wss:" : "ws:"; + } else { + url = new URL(`ws://${input.replace(/\/+$/, "")}`); + } + + if (!url.pathname.endsWith("/centrallinkws/")) { + url.pathname = `${url.pathname.replace(/\/+$/, "")}/centrallinkws/`; + } + return url.toString(); +} + +function concatBytes(...parts: Array): Buffer { + return Buffer.concat(parts.map((part) => Buffer.from(part))); +} + +function bnBytes(bn: BN, length = 32): Buffer { + return Buffer.from(bn.toArray("be", length)); +} + +function sha256Bytes(buf: Buffer | Uint8Array): Buffer { + return createHash("sha256").update(Buffer.from(buf)).digest(); +} + +class ECJPAKE { + role: string; + peerRole: string; + secret: BN; + curveName: string; + serverRoundTwoPrefix = 22; + failed = false; + wroteRoundOne = false; + readRoundOneDone = false; + wroteRoundTwo = false; + readRoundTwoDone = false; + x1: ReturnType["genKeyPair"]> | null = null; + x2: ReturnType["genKeyPair"]> | null = null; + peerX1: ReturnType["keyFromPublic"]>["pub"] | null = null; + peerX2: ReturnType["keyFromPublic"]>["pub"] | null = null; + + constructor({ role, secret }: { role: string; secret: string }) { + if (role !== "server" && role !== "client") { + throw new TypeError('role must be "client" or "server"'); + } + if (typeof secret !== "string") { + throw new TypeError("secret must be a string"); + } + this.role = role; + this.peerRole = role === "client" ? "server" : "client"; + this.secret = new BN(Buffer.from(secret, "utf8")); + this.curveName = "secp256k1"; + } + + writeRoundOne(): Buffer { + this.assertUsable(); + if (this.wroteRoundOne || this.wroteRoundTwo || this.readRoundTwoDone) { + throw new Error("Wrong step"); + } + this.wroteRoundOne = true; + const ec = new EllipticEC(this.curveName); + this.x1 = ec.genKeyPair(); + this.x2 = ec.genKeyPair(); + const zkp1 = this.createZkp(ec, ec.g, this.x1.getPublic(), this.x1.getPrivate(), this.role); + const zkp2 = this.createZkp(ec, ec.g, this.x2.getPublic(), this.x2.getPrivate(), this.role); + return concatBytes( + this.encodePublic(this.x1.getPublic()), + this.encodeZkp(zkp1), + this.encodePublic(this.x2.getPublic()), + this.encodeZkp(zkp2), + ); + } + + readRoundOne(payload: Uint8Array): void { + this.assertUsable(); + if (this.readRoundOneDone || this.wroteRoundTwo || this.readRoundTwoDone) { + throw new Error("Wrong step"); + } + this.readRoundOneDone = true; + const ec = new EllipticEC(this.curveName); + let offset = 0; + + const peerX1Len = payload[offset++]!; + const peerX1Wire = payload.slice(offset, offset + peerX1Len); + offset += peerX1Len; + this.peerX1 = ec.keyFromPublic(Buffer.from(peerX1Wire)).getPublic(); + + const zkp1Length = 1 + payload[offset]! + 1 + payload[offset + 1 + payload[offset]!]!; + const zkp1 = this.decodeZkp(payload.slice(offset, offset + zkp1Length)); + offset += zkp1Length; + if (!this.verifyZkp(ec, ec.g, this.peerX1!, zkp1.V, zkp1.r, this.peerRole)) { + this.failed = true; + throw new Error("ECJPAKE round one failed"); + } + + const peerX2Len = payload[offset++]!; + const peerX2Wire = payload.slice(offset, offset + peerX2Len); + offset += peerX2Len; + this.peerX2 = ec.keyFromPublic(Buffer.from(peerX2Wire)).getPublic(); + + const zkp2Length = 1 + payload[offset]! + 1 + payload[offset + 1 + payload[offset]!]!; + const zkp2 = this.decodeZkp(payload.slice(offset, offset + zkp2Length)); + if (!this.verifyZkp(ec, ec.g, this.peerX2!, zkp2.V, zkp2.r, this.peerRole)) { + this.failed = true; + throw new Error("ECJPAKE round one failed"); + } + } + + writeRoundTwo(): Buffer { + this.assertUsable(); + if (this.wroteRoundTwo || !this.wroteRoundOne || !this.readRoundOneDone) { + throw new Error("Wrong step"); + } + this.wroteRoundTwo = true; + const ec = new EllipticEC(this.curveName); + const generator = this.x1!.getPublic().add(this.peerX1!).add(this.peerX2!); + ec.g = generator; + const n = new BN(randomBytes(16)).mul(ec.n).add(this.secret); + const s = this.x2!.getPrivate().mul(n).umod(ec.n); + const publicKey = generator.mul(s); + const zkp = this.createZkp(ec, generator, publicKey, s, this.role); + const encodedPublic = this.encodePublic(publicKey); + const encodedZkp = this.encodeZkp(zkp); + + if (this.role === "server") { + return concatBytes(this.encodeServerRoundTwoPrefix(), encodedPublic, encodedZkp); + } + return concatBytes(encodedPublic, encodedZkp); + } + + readRoundTwo(payload: Uint8Array): Buffer { + this.assertUsable(); + if (this.readRoundTwoDone || !this.wroteRoundOne || !this.readRoundOneDone) { + throw new Error("Wrong step"); + } + this.readRoundTwoDone = true; + const ec = new EllipticEC(this.curveName); + const generator = this.x1!.getPublic().add(this.x2!.getPublic()).add(this.peerX1!); + let offset = this.role === "client" ? 3 : 0; + + const publicLen = payload[offset++]!; + const publicWire = payload.slice(offset, offset + publicLen); + offset += publicLen; + const peerPublic = ec.keyFromPublic(Buffer.from(publicWire)).getPublic(); + + const zkpLength = 1 + payload[offset]! + 1 + payload[offset + 1 + payload[offset]!]!; + const zkp = this.decodeZkp(payload.slice(offset, offset + zkpLength)); + if (!this.verifyZkp(ec, generator, peerPublic, zkp.V, zkp.r, this.peerRole)) { + this.failed = true; + throw new Error("ECJPAKE round two failed"); + } + + const n = new BN(randomBytes(16)).mul(ec.n).add(this.secret); + const m = this.x2!.getPrivate().mul(n).umod(ec.n); + const sharedPoint = peerPublic.add(this.peerX2!.mul(m).neg()).mul(this.x2!.getPrivate()); + return sha256Bytes(bnBytes(sharedPoint.getX(), 32)); + } + + assertUsable(): void { + if (this.failed) { + throw new Error("Reusing failed ECJPAKE context is insecure."); + } + } + + encodePointForHash(point: InstanceType["point"]): Buffer { + const out = Buffer.alloc(69); + out.writeUInt32BE(65, 0); + out[4] = 4; + bnBytes(point.getX(), 32).copy(out, 5); + bnBytes(point.getY(), 32).copy(out, 37); + return out; + } + + encodePublic(point: InstanceType["point"]): Buffer { + const out = Buffer.alloc(66); + out[0] = 65; + out[1] = 4; + bnBytes(point.getX(), 32).copy(out, 2); + bnBytes(point.getY(), 32).copy(out, 34); + return out; + } + + encodeServerRoundTwoPrefix(): Buffer { + const out = Buffer.alloc(3); + out[0] = 3; + out.writeUInt16BE(this.serverRoundTwoPrefix, 1); + return out; + } + + zkpChallenge( + generator: InstanceType["point"], + v: InstanceType["point"], + x: InstanceType["point"], + signerId: string, + order: BN, + ): BN { + const id = Buffer.from(signerId, "utf8"); + const len = Buffer.alloc(4); + len.writeUInt32BE(id.length, 0); + const hashInput = concatBytes( + this.encodePointForHash(generator), + this.encodePointForHash(v), + this.encodePointForHash(x), + len, + id, + ); + return new BN(sha256Bytes(hashInput).toString("hex"), "hex", "be").umod(order); + } + + createZkp( + ec: InstanceType, + generator: InstanceType["point"], + publicKey: InstanceType["point"], + privateKey: BN, + signerId: string, + ): { V: InstanceType["point"]; r: BN } { + const zkpEc = new EllipticEC(this.curveName); + zkpEc.g = generator; + const vKey = zkpEc.genKeyPair(); + const challenge = this.zkpChallenge(zkpEc.g, vKey.getPublic(), publicKey, signerId, zkpEc.n); + const r = vKey.getPrivate().sub(challenge.mul(privateKey)).umod(zkpEc.n); + return { V: vKey.getPublic(), r }; + } + + encodeZkp(zkp: { V: InstanceType["point"]; r: BN }): Buffer { + const out = Buffer.alloc(99); + this.encodePublic(zkp.V).copy(out, 0); + out[66] = 32; + bnBytes(zkp.r, 32).copy(out, 67); + return out; + } + + decodeZkp(payload: Uint8Array): { V: InstanceType["point"]; r: BN } { + const ec = new EllipticEC(this.curveName); + const publicLen = payload[0]!; + const publicWire = payload.slice(1, 1 + publicLen); + const rLenOffset = 1 + publicLen; + const rLen = payload[rLenOffset]!; + const rBytes = payload.slice(rLenOffset + 1, rLenOffset + 1 + rLen); + return { + V: ec.keyFromPublic(Buffer.from(publicWire)).getPublic(), + r: new BN(Buffer.from(rBytes)), + }; + } + + verifyZkp( + ec: InstanceType, + generator: InstanceType["point"], + publicKey: InstanceType["point"], + v: InstanceType["point"], + r: BN, + signerId: string, + ): boolean { + const verifyEc = new EllipticEC(this.curveName); + verifyEc.g = generator; + const challenge = this.zkpChallenge(generator, v, publicKey, signerId, verifyEc.n); + return publicKey.mul(challenge).add(generator.mul(r)).eq(v); + } +} + +class CounterGcm { + key: Buffer; + salt: Buffer; + selfCounter = 1; + peerCounter = 0; + + constructor(key: Buffer, salt: Buffer) { + this.key = Buffer.from(key); + this.salt = Buffer.from(salt); + if (this.key.length !== 16) throw new Error("AES key must be 16 bytes"); + if (this.salt.length !== 8) throw new Error("AES salt must be 8 bytes"); + } + + iv(counter: number): Buffer { + const iv = Buffer.alloc(12); + this.salt.copy(iv, 0); + iv.writeUInt32LE(counter, 8); + return iv; + } + + encrypt(plain: Buffer | Uint8Array): Buffer { + const counter = this.selfCounter++; + const cipher = createCipheriv("aes-128-gcm", this.key, this.iv(counter), { + authTagLength: 16, + }); + const ciphertext = Buffer.concat([cipher.update(Buffer.from(plain)), cipher.final()]); + const tag = cipher.getAuthTag(); + const out = Buffer.alloc(4 + ciphertext.length + tag.length); + out.writeUInt32LE(counter, 0); + ciphertext.copy(out, 4); + tag.copy(out, 4 + ciphertext.length); + return out; + } + + decrypt(frame: Buffer | Uint8Array): Buffer { + const input = Buffer.from(frame); + const counter = input.readUInt32LE(0); + if (counter <= this.peerCounter) { + throw new Error(`Replay or out-of-order frame: ${counter}`); + } + this.peerCounter = counter; + const tag = input.subarray(input.length - 16); + const ciphertext = input.subarray(4, input.length - 16); + const decipher = createDecipheriv("aes-128-gcm", this.key, this.iv(counter), { + authTagLength: 16, + }); + decipher.setAuthTag(tag); + return Buffer.concat([decipher.update(ciphertext), decipher.final()]); + } +} + +function pack(type: number, payload: Buffer = Buffer.alloc(0)): Buffer { + return Buffer.concat([Buffer.from([type]), Buffer.from(payload)]); +} + +function compressJson(obj: unknown): Buffer { + const raw = Buffer.from(JSON.stringify(obj), "utf8"); + const deflated = deflateRawSync(raw); + const out = Buffer.alloc(4 + deflated.length); + out.writeUInt32LE(raw.length, 0); + deflated.copy(out, 4); + return out; +} + +function decompressJson(payload: Buffer | Uint8Array): unknown { + const input = Buffer.from(payload); + const expectedLength = input.readUInt32LE(0); + const raw = inflateRawSync(input.subarray(4)); + if (raw.length !== expectedLength) { + throw new Error(`Inflated JSON length mismatch: ${raw.length} != ${expectedLength}`); + } + return JSON.parse(raw.toString("utf8")); +} + +async function wsDataToBuffer(data: unknown): Promise { + if (typeof data === "string") { + throw new Error("Unexpected websocket text frame"); + } + if (data instanceof ArrayBuffer) return Buffer.from(data); + if (ArrayBuffer.isView(data)) { + return Buffer.from(data.buffer, data.byteOffset, data.byteLength); + } + if (data && typeof (data as Blob).arrayBuffer === "function") { + return Buffer.from(await (data as Blob).arrayBuffer()); + } + return Buffer.from(data as ArrayBufferView); +} + +class GatewayClient { + url: string; + passcode: string; + stage = "init"; + ecjpake: ECJPAKE | null = null; + sessionCipher: CounterGcm | null = null; + outCipher: CounterGcm | null = null; + inCipher: CounterGcm | null = null; + ready = false; + nextId = 0; + pending = new Map void; reject: (reason: Error) => void; timeout: ReturnType }>(); + ws!: WebSocket; + + constructor(url: string, passcode: string) { + this.url = url; + this.passcode = passcode; + } + + async connect(): Promise { + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error("WebSocket connect timeout")), 10000); + this.ws = new WebSocket(this.url); + this.ws.binaryType = "arraybuffer"; + + this.ws.onopen = () => { + clearTimeout(timeout); + this.sendProtocolList(); + }; + this.ws.onerror = () => { + clearTimeout(timeout); + reject(new Error(`WebSocket error while connecting to ${this.url}`)); + }; + this.ws.onclose = (event) => { + if (!this.ready) { + clearTimeout(timeout); + reject(new Error(`WebSocket closed before ready: ${event.code} ${event.reason}`)); + } + }; + this.ws.onmessage = async (event) => { + try { + this.handleMessage(await wsDataToBuffer(event.data)); + if (this.ready) resolve(); + } catch (err) { + reject(err); + } + }; + }); + } + + close(): void { + try { + this.ws?.close(); + } catch {} + } + + send(buf: Buffer): void { + this.ws.send(Buffer.from(buf)); + } + + log(message: string): void { + if (process.env.MIOT_TRACE === "1") console.error(`[trace] ${message}`); + } + + sendProtocolList(): void { + this.stage = "protocol-list"; + this.log("send PROTOCOL_LIST"); + this.send(pack(DATA_TYPE.PROTOCOL_LIST, Buffer.from(JSON.stringify(["passcode"])))); + } + + handleMessage(frame: Buffer): void { + const type = frame[0]!; + const payload = frame.subarray(1); + this.log(`recv type=${type} len=${frame.length}`); + + switch (type) { + case DATA_TYPE.SELECTED_PROTOCOL: + return this.handleSelectedProtocol(payload); + case DATA_TYPE.ECJPAKE_ROUND_ONE: + return this.handleRoundOne(payload); + case DATA_TYPE.ECJPAKE_ROUND_TWO: + return this.handleRoundTwo(payload); + case DATA_TYPE.SESSION_KEY_EXCHANGE: + return this.handleSessionKeyExchange(payload); + case DATA_TYPE.DATA: + return this.handleData(payload); + case DATA_TYPE.ERROR: + throw new Error(`Gateway returned ERROR frame during ${this.stage}`); + default: + throw new Error(`Unexpected frame type: ${type}`); + } + } + + handleSelectedProtocol(payload: Buffer): void { + this.stage = "selected-protocol"; + const selected = JSON.parse(payload.toString("utf8")) as { protocol: string }; + if (selected.protocol !== "passcode") { + throw new Error(`Gateway selected unsupported protocol: ${selected.protocol}`); + } + this.ecjpake = new ECJPAKE({ role: "client", secret: this.passcode }); + this.stage = "ecjpake-round-one"; + this.log("send ECJPAKE_ROUND_ONE"); + this.send(pack(DATA_TYPE.ECJPAKE_ROUND_ONE, this.ecjpake.writeRoundOne())); + } + + handleRoundOne(payload: Buffer): void { + this.stage = "ecjpake-round-two"; + this.ecjpake!.readRoundOne(new Uint8Array(payload)); + this.log("send ECJPAKE_ROUND_TWO"); + this.send(pack(DATA_TYPE.ECJPAKE_ROUND_TWO, this.ecjpake!.writeRoundTwo())); + } + + handleRoundTwo(payload: Buffer): void { + this.stage = "session-key-exchange"; + const shared = Buffer.from(this.ecjpake!.readRoundTwo(new Uint8Array(payload))); + this.sessionCipher = new CounterGcm(shared.subarray(0, 16), shared.subarray(16, 24)); + + const localMaterial = randomBytes(24); + this.outCipher = new CounterGcm(localMaterial.subarray(0, 16), localMaterial.subarray(16, 24)); + this.log("send SESSION_KEY_EXCHANGE"); + this.send(pack(DATA_TYPE.SESSION_KEY_EXCHANGE, this.sessionCipher.encrypt(localMaterial))); + } + + handleSessionKeyExchange(payload: Buffer): void { + this.stage = "secure-session"; + const remoteMaterial = this.sessionCipher!.decrypt(payload); + this.inCipher = new CounterGcm(remoteMaterial.subarray(0, 16), remoteMaterial.subarray(16, 24)); + this.ready = true; + this.log("secure session ready"); + } + + handleData(payload: Buffer): void { + const rpc = decompressJson(this.inCipher!.decrypt(payload)) as { id: number; error?: { message: string }; result?: unknown }; + const key = String(rpc.id); + const pending = this.pending.get(key); + if (!pending) return; + + clearTimeout(pending.timeout); + this.pending.delete(key); + if ("error" in rpc && rpc.error) { + pending.reject(new Error(rpc.error?.message || JSON.stringify(rpc.error))); + } else { + pending.resolve(rpc.result); + } + } + + async callAPI(method: string, params: Record = {}, timeoutMs = 10000): Promise { + if (!this.ready) throw new Error("Secure session is not ready"); + this.stage = `api:${method}`; + const id = this.nextId++; + const rpc = { + jsonrpc: "2.0", + id, + method: `/api/${method}`, + params, + }; + const encrypted = this.outCipher!.encrypt(compressJson(rpc)); + this.send(pack(DATA_TYPE.DATA, encrypted)); + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + this.pending.delete(String(id)); + reject(new Error(`API timeout: ${method}`)); + }, timeoutMs); + this.pending.set(String(id), { resolve, reject, timeout }); + }); + } +} + +function getOutputPath(): string { + return resolve(dirname(import.meta.file), "..", "devices.json"); +} + +async function main(): Promise { + const args = process.argv.slice(2); + if (args.includes("-h") || args.includes("--help")) usage(0); + if (args[0] === "--self-test") { + const client = new ECJPAKE({ role: "client", secret: "000000" }); + const server = new ECJPAKE({ role: "server", secret: "000000" }); + const clientRoundOne = client.writeRoundOne(); + const serverRoundOne = server.writeRoundOne(); + if (clientRoundOne.length !== 330 || serverRoundOne.length !== 330) { + throw new Error( + `Unexpected ECJPAKE round-one lengths: ${clientRoundOne.length}, ${serverRoundOne.length}`, + ); + } + client.readRoundOne(serverRoundOne); + server.readRoundOne(clientRoundOne); + const clientRoundTwo = client.writeRoundTwo(); + const serverRoundTwo = server.writeRoundTwo(); + if (clientRoundTwo.length !== 165 || serverRoundTwo.length !== 168) { + throw new Error( + `Unexpected ECJPAKE round-two lengths: ${clientRoundTwo.length}, ${serverRoundTwo.length}`, + ); + } + const clientKey = Buffer.from(client.readRoundTwo(serverRoundTwo)); + const serverKey = Buffer.from(server.readRoundTwo(clientRoundTwo)); + if (clientKey.length !== 32 || !clientKey.equals(serverKey)) { + throw new Error("ECJPAKE self-test shared key mismatch"); + } + console.error("Self-test OK"); + return; + } + if (args.length !== 2) usage(1); + + const url = normalizeUrl(args[0]!); + const passcode = args[1]!; + if (!/^\d{6}$/.test(passcode)) { + throw new Error("Passcode must be a 6-digit string"); + } + + const outputPath = getOutputPath(); + console.error(`Connecting to ${url}`); + const client = new GatewayClient(url, passcode); + try { + await client.connect(); + console.error("Secure session established"); + const result = (await client.callAPI("getDevList", {}, 10000)) as { devList?: Record }; + const devList = result.devList || {}; + const payload = { + fetchedAt: new Date().toISOString(), + url, + count: Object.keys(devList).length, + devList, + }; + writeFileSync(outputPath, `${JSON.stringify(payload, null, 2)}\n`); + console.error(`已更新设备清单: ${outputPath} (${Object.keys(devList).length} 台设备)`); + } finally { + client.close(); + } +} + +main().catch((err: unknown) => { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; +}); diff --git a/src/types.ts b/src/types.ts index 58da6c4..b098f9c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -220,19 +220,12 @@ export interface RuleDefinition { name: string; id?: string; enable: boolean; - replace?: RuleReplaceStrategy; workflow: WorkflowExpr; } -export type RuleReplaceStrategy = - | { mode: "append" } - | { mode: "replaceById"; id: string } - | { mode: "replaceByName"; name?: string }; - export interface RuleOptions { id?: string; enable?: boolean; - replace?: RuleReplaceStrategy; } export interface GraphNode { @@ -288,10 +281,8 @@ export interface CompileResult { export interface CompileSummary { addedRules: string[]; - replacedRules: string[]; nodeCount: number; deviceDids: string[]; - outputPath?: string; } export type DiagnosticSeverity = "error" | "warning"; diff --git a/tests/compiler.test.ts b/tests/compiler.test.ts index c2fd8e5..c82f9aa 100644 --- a/tests/compiler.test.ts +++ b/tests/compiler.test.ts @@ -1,10 +1,10 @@ import { describe, expect, test } from "bun:test"; -import { readDeviceInventoryFile, readBackupFile } from "../src/index.ts"; +import { readBackupFile } from "../src/index.ts"; import { collectRuleDefinitions, compileRule, compileWorkflow } from "../src/compiler.ts"; -import examples, { conditionBeforeAction, eventTurnOn, mergedTriggers } from "../examples/rules/index.ts"; +import examples, { conditionBeforeAction, eventTurnOn, mergedTriggers } from "../src/rules/index.ts"; const now = 1_760_000_000_000; -const devices = readDeviceInventoryFile("resources/devices.json"); +const devices = (await import("../src/devices.json")).default; describe("规则编译", () => { test("单条事件触发开灯规则生成稳定 JSON", () => { @@ -79,16 +79,8 @@ describe("规则编译", () => { }); test("模块导出收集支持默认数组和命名规则", async () => { - const moduleExports = await import("../examples/rules/index.ts"); + const moduleExports = await import("../src/rules/index.ts"); const definitions = collectRuleDefinitions(moduleExports); expect(definitions.map((definition) => definition.name)).toEqual(examples.map((definition) => definition.name)); }); - - test("编译并追加到真实备份时保留原有规则", () => { - const base = readBackupFile("resources/备份2026_4_26 19_17_42.bak"); - const result = compileWorkflow([eventTurnOn], base, { now, devices }); - expect(result.diagnostics.filter((diagnostic) => diagnostic.severity === "error")).toEqual([]); - expect(result.backup.rules).toHaveLength(base.rules.length + 1); - expect(result.backup.rules.at(-1)?.cfg.userData.name).toBe("示例-事件触发开灯"); - }); }); diff --git a/tests/validate.test.ts b/tests/validate.test.ts index 15d490f..1b1dfde 100644 --- a/tests/validate.test.ts +++ b/tests/validate.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { compileRule, readDeviceInventoryFile, validateBackup } from "../src/index.ts"; -import { eventTurnOn } from "../examples/rules/index.ts"; +import { compileRule, validateBackup } from "../src/index.ts"; +import { eventTurnOn } from "../src/rules/index.ts"; const now = 1_760_000_000_000; @@ -25,9 +25,10 @@ describe("备份校验", () => { expect(diagnostics.filter((diagnostic) => diagnostic.code === "device-not-found")).toHaveLength(2); }); - test("真实设备清单可通过设备校验", () => { + test("真实设备清单可通过设备校验", async () => { const backup = backupFromRule(); - const diagnostics = validateBackup(backup, { devices: readDeviceInventoryFile("resources/devices.json") }); + const devices = (await import("../src/devices.json")).default; + const diagnostics = validateBackup(backup, { devices }); expect(diagnostics.filter((diagnostic) => diagnostic.severity === "error")).toEqual([]); }); });