Files
iboard/docs/V0.1/环境监控模块设计.md
T
jif2.zhang 3de002aae8 修复米家会话失效后不弹登录二维码
根因一: 过期检测形同虚设
- @zythum02/mijia-api 的 refreshToken() 在本地凭证字段齐全时直接返回成功,
  根本不发请求, 因此 checkAuth() 永远判定 ok, 过期后既不出横幅也不出扫码入口
- checkAuth() 改为真打一次 getHomesList() 验证 token; 成功缓存 5 分钟,
  失败只缓存 30 秒(便于网络恢复后自愈); 增加并发探测合并; 状态未变不写库
- getStatus() 以实时探测结果为准, DB 里的 status 仅在探测为 unknown 时兜底,
  避免残留的 ok 遮蔽已失效状态

根因二: 二维码图片 401 导致弹窗空白
- /api/mihome/reauthorize/qr-image 在 authRequired 之后, 而 <img src> 无法携带
  Authorization 头, 图片请求被拒, 用户看到的是空白/裂图
- 新增 fetchMihomeQrImage(): 带 JWT fetch 后转 blob URL 给 <img>, 并在卸载/换码时释放

其他修复
- EnvView 的 load 依赖 [status] 且成功回调又 setStatus, 造成 /status 无限重复请求;
  改为空依赖 + 函数式 setStatus; 首屏与重试走 ?force=1 强查
- 会话失效(非网络类错误)时自动弹出扫码窗, 每次挂载仅弹一次
- 扫码状态机新增 done 标记, 只有真正登录成功才算成功, 避免取消后误判
- 二维码 120s 超时自动续码(最多 2 次), 慢扫不再直接失败
- 取消后不再丢弃可能已成功的扫码结果
- scripts/mihome-login.mjs 适配 done 语义
- 同步更新设计文档 §5.2 / §5.4 / §6

注意: 前端需重新构建发布后生效。
2026-10-24 07:26:59 +08:00

24 KiB
Raw Blame History

环境监控模块设计(米家温湿度长期记录)

目标项目: iboard(Tauri + React + Express + Prisma/MariaDB) 状态: 设计稿 v1.1 · 2026-09-20 · 待评审(v1.1: 视图新增 今天/月/年三档粒度 + 标题显示电量)

1. 需求与目标

在监控页新增「环境监控」tab,实现多家庭下温湿度长期记录:

  1. 监控页新增一个 tab「环境监控」。
  2. 多家庭支持:每个家庭一个可折叠 group,手风琴式展开(同一时间只能展开一个)。
  3. 每个设备可独立修改别名(仅本应用展示用,不改动米家云端设备名)。
  4. 数据非实时(定时批量,不做推送):每小时采样 1 次落库小时档,每日聚合成日档。
  5. 记录保留永久(无清理任务)。
  6. 缺失补齐语义(用户确认):
    • 应用停机导致的缺失 → 应用恢复后自动重新获取补齐(有统计的型号经云端聚合真实回填, 见 §2.2B;无统计的型号保持缺口标注);
    • 设备自身原因(离线/故障)的缺失 → 保持缺失,不伪造数据;
    • 总原则:本地数据不少于米家可提供的数据。
  7. 米家授权失效 → 前端显示「重新授权」按钮,点击弹出二维码,人工扫码后恢复。
  8. 每设备数据默认显示当天,粒度小时。
  9. 每设备数据可切换为月,粒度天。
  10. 每设备数据可切换为年,粒度周。
  11. 设备卡片标题 = 别名 + 电量(电量取当天缓存值,不必频繁更新)。

已确认的数据范围:仅温湿度(不含照度 / 人体存在)。

2. 已验证事实(调研结论,作为设计地基)

调研用的账号 18814803393,登录链路与数据能力已实测(脚本位于 C:\Users\xydkj\AppData\Local\Temp\opencode\mibridge-research\):

项目 结论 证据
登录方式 密码登录被米家拒绝(70016);扫码登录可行 qr-once.mjs
Token 有效期 30 天,DiskCache 持久化,脚本自动加载刷新 .mijia-cache/
实时读值 api.getDevicesProp([{did,siid,piid}]) 稳定可用,100 项请求 10 轮 100% 成功 env-monitor.mjs
单轮延迟 p50 ≈ 520ms,p95 ≈ 612ms(10 项属性/轮) 同上
VRF 空调目标温写入 整数生效但生效延迟 5~6s(updateTime 才更新);半度 26.5 被服务端拒收(-704220043) vrf-temp-test*.mjs
温湿度历史读取 不可读:history-data(8.1)/start-time(8.3) 返回 -704010000;start-synic 动作要求参数;/v2/home/device/prop_history 类端点不可达 dev-history-probe.mjs

2.1 环境设备清单(共 3 台,均属家庭「黄金农场」505001017889)

家庭 设备 型号 did 温度 湿度 电量
黄金农场 炎米智能温湿度计 ym001.sensor_ht.ymwsdj blt.3.1nmu8afgt0g00 2.1001 2.1002 3.1003
黄金农场 炎米智能温湿度计 2 ym001.sensor_ht.ymwsdj blt.3.1nmu8eunt0001 2.1001 2.1002 3.1003
黄金农场 闹钟(青萍,含温湿) cgllc.clock.dove blt.3.12cq3lu8glo00 2.1 2.2 2.3

第二个家庭「韩家」(586001550228) 当前无环境设备,设计需容忍空家庭。

2.2 关键约束:逐点历史不可达,但云端「聚合统计历史」可用(实测反转)

原结论"云端历史完全不可达"已推翻。实测分两条:

A. 逐点历史(设备本地日志)云 API 不可达

  • 炎米温湿度计:规格含 8.1 history-data / 8.3 start-time / start-synic,但 8.1/8.3 读取全返回 -704010000(不可读),start-synic 动作试了 8 种参数形态全部报 -704220025 参数个数不匹配, 判定为"局域网直连通道",云侧无暴露;
  • 青萍闹钟:miot 规格仅 temperature / relative-humidity / battery-level,无任何历史属性/动作。

B. 云端聚合统计历史「可用」(%@zythum02/mijia-api 可直接打 POST /v2/user/statistics) 参数 { did, key(siid.piid), data_type, limit, time_start, time_end },用本库 api.request() 直连即可。

data_type 桶(Asia/Shanghai) value 语义 实测(炎米 ym001.sensor_ht.ymwsdj)
stat_hour_v3 1 小时整点 小时均值 连续入库(炎米1 有部分缺档),如 [28.8]
stat_day_v3 1 天 00:00 当日 [min, max] 45 天全有,如 [28.1, 29.2],与实时读值吻合
stat_month_v3 1 月 当月 [min, max] 回溯到 2026-01(联网注册后云端一直记账)
stat_week_v3 1 周 — 该型号返回 -8 invalid params(未开通周档)
回补深度 — — 两台炎米均回溯到接入米家日起(实测到 2026-01-03);缺席日=设备当天无上报
  • 青萍闹钟 cgllc.clock.dove 与 VRF proxy.624017060.00001 全部 data_type 报 -8 invalid params → 云侧未为其开通统计(青萍与其无历史服务一致)。
  • 证据:mibridge-research/stats-probe.mjs / stats-probe2.json。

因此:

  • 停机缺口可补真值:对开通统计的型号(炎米等),停机恢复后用 stat_day_v3 回填缺日 [min,max]、stat_hour_v3 补小时均值,origin=vendor_history(见 §5.3);
  • 「精确到某个刻的逐点值」仍不可得(那是设备本地日志,须走局域网 BLE/网关,见 §10);
  • 未开通统计的型号(青萍)维持「当天实时采样 + 缺口标注」。

3. 总体方案

  • 后端新增 server/mihome/ 模块,封装与米家的认证 + 采集 + 查询;
  • 依赖新增 @zythum02/mijia-api(扫码登录、getDevicesProp、DiskCache), 认证缓存文件用 DiskCache 存于服务端磁盘(.cache/),与采集进程同机;
  • 每小时采样 1 次写小时档(MihomeEnvHour),每日 03:00 (Asia/Shanghai) 聚合日档 (均值/min/max)+ 读一次电量;服务启动时做追赶同步(缺小时/缺天则按序补齐), 停机跨天的历史缺口由 vendorHistoryFiller 用 /v2/user/statistics 真实回填(有统计的型号);
  • 前端 MonitorPage 增加 tab,antd-mobile Collapse(手风琴)+ recharts 趋势图, 别名用 Dialog.Prompt 编辑;每设备卡片标题=别名+电量,展开后 Segmented 切换 今天(时)/月(天)/年(周) 三档视图;
  • 数据永久保存,小时档按 (deviceId, hour)、日档按 (deviceId, date) 唯一,天然防重。

4. 数据模型(Prisma)

// ===== 环境监控:米家 =====

// 米家授权状态(DiskCache 存凭证, 本表只存状态/错误供前端展示)
model MihomeAccount {
  id        Int      @id @default(autoincrement())
  key       String   @unique @default("default")
  userId    String   @default("")
  status    String   @default("unknown") // ok | pending | expired
  lastError String   @default("")
  updatedAt DateTime @updatedAt
}

model MihomeHome {
  id        Int            @id @default(autoincrement())
  homeId    String         @unique
  name      String
  createdAt DateTime       @default(now())
  devices   MihomeDevice[]
}

model MihomeDevice {
  id         Int            @id @default(autoincrement())
  did        String         @unique
  homeId     String
  home       MihomeHome     @relation(fields: [homeId], references: [homeId])
  model      String
  name       String
  alias      String?                  // 用户别名, 仅本应用展示
  tempSiid   Int
  tempPiid   Int
  humSiid    Int
  humPiid    Int
  battSiid   Int                      // 电池电量属性(如炎米3.1003 / 青萍2.3)
  battPiid   Int
  battery    Int?                     // 最近一次电量(每天读一次, 标题展示用)
  batteryAt  DateTime?                // 电量读取时间(当天缓存, 不频繁更新)
  enabled    Boolean        @default(true)
  lastSyncAt DateTime?
  createdAt  DateTime       @default(now())
  samples    MihomeEnvSample[]
  hours      MihomeEnvHour[]
}

model MihomeEnvSample {
  id          Int          @id @default(autoincrement())
  deviceId    Int
  device      MihomeDevice @relation(fields: [deviceId], references: [id], onDelete: Cascade)
  date        DateTime              // 采样日(Asia/Shanghai 当日 00:00 存 UTC)
  temperature Float?                // 当日最后一次 live 值(或 vendor hour 均值)
  humidity    Float?
  temperatureAvg Float?             // 当日均值(由小时档聚合, 或 vendor_hour)
  humidityAvg    Float?
  temperatureMin Float?             // 当日最低
  temperatureMax Float?             // 当日最高
  humidityMin    Float?
  humidityMax    Float?
  origin      String       @default("live")   // live | vendor_history | backfill
  sampledAt   DateTime?
  raw         Json?
  createdAt   DateTime     @default(now())

  @@unique([deviceId, date])                 // 一天一条, 防重
  @@index([deviceId, date])
}

model MihomeEnvHour {
  id             Int          @id @default(autoincrement())
  deviceId       Int
  device         MihomeDevice @relation(fields: [deviceId], references: [id], onDelete: Cascade)
  hour           DateTime              // Asia/Shanghai 整点(存 UTC)
  temperatureAvg Float?                // 该小时均值(live 采样点聚合, 或 vendor_hour)
  humidityAvg    Float?
  raw            Json?
  createdAt      DateTime     @default(now())

  @@unique([deviceId, hour])                 // 一小时一条, 防重
  @@index([deviceId, hour])
}

要点:

  • 凭证不进库,只在密钥盘(server/mihome/.cache/,需加入 .gitignore);
  • MihomeHome / MihomeDevice 随每次同步 upsert(以米家为准,重扫不漏不错);
  • 存储分三档:小时档(当天视图,逐小时)、日档(月视图,日均/min/max)、 年视图按日档现场聚合周桶(不落库,见 §5.6);
  • 电量只存在 MihomeDevice.battery/batteryAt,每天读 1 次覆盖当天展示用(需求 11);
  • MihomeEnvSample.origin:
    • live——定时采样的真实值(小时档写入,日档由小时聚合);
    • vendor_history——经 /v2/user/statistics 拉回的云端聚合历史(min/max/hour 均值);
    • backfill——停机恢复后对当天的补记(仍用真实值,与 live 同字段)。

5. 后端设计

5.1 代码结构

server/mihome/
  index.js        # 模块入口: 初始化 MijiaAPI + DiskCache, 导出 service
  auth.js         # 认证管理 + 扫码流程(后台 QRlogin 循环 + pendingQr 状态机)
  collector.js    # 采集器: 每小时采样 / 每日聚合 / 追赶同步 / 电量 / 设备发现
  vendorStats.js  # /v2/user/statistics 封装(按型号探测可用性, 提供 hour/day 回填)
  history.js      # 查询聚合: today(时) / month(天) / year(周)
  cache.js        # DiskCache 路径与清理
server/routes/mihome.js   # REST 路由 (authRequired)

5.2 认证与「重新授权」

  • 启动时加载 DiskCache 凭证,并用一次真实云请求(getHomesList)确认 token 仍有效; 只有该请求通过才 status=ok。
  • 失败(30 天过期 / 风控 / 无凭证)→ MihomeAccount.status=expired,前端弹授权横幅 + 扫码窗。
  • 重新授权流程(服务端驱动,前端轮询):
POST /api/mihome/reauthorize
  └─ 若已有流程在跑 => 直接复用(同一张二维码继续有效)
  └─ 否则后台启动 QRlogin 循环(每轮 120s 超时自动续二维码, 最多续 2 次)
  └─ 立即返回 { running, done, state, qrImageUrl }

GET /api/mihome/reauthorize/status
  └─ 返回 { running, done, state: idle|waiting|failed, qrImageUrl?, lastError? }
  └─ done=true 表示扫码成功、凭证已落盘、authCache 已置 ok

POST /api/mihome/reauthorize/cancel   # 人工取消: 只让前端停止轮询, 后台长轮询自然结束
  • qrState 为模块级单例,防并发(同一时间只允许一个扫码流程)。
  • 前端在弹窗内 setInterval 3s 轮询,遇 done=true 关弹窗并刷新列表;failed 则停轮询显示错误。
  • 二维码图片接口在 authRequired 之后,<img> 带不上 JWT,前端必须 fetch 后转 blob: URL 显示。

5.3 采集器 collector

  1. 预检:whoami().login 不为真 → 置 expired,本轮跳过(不产生记录)。
  2. 设备发现:getDevicesList() → upsert 家庭/设备;仅保留具备 temperature + relative-humidity 属性的型号(经 getDeviceInfo/miot-spec 判定, 当前命中 ym001.sensor_ht.ymwsdj、cgllc.clock.dove 白名单);记录其 battSiid/battPiid。
  3. 小时采样(每小时):批量 getDevicesProp 读温度+湿度 → upsert MihomeEnvHour(deviceId, hour=当前整点),写 temperatureAvg/humidityAvg(此小时第一个有效点 即视为小时值)。每台独立 try/catch:
    • 设备离线/超时 → 该小时不写(缺口保持);
    • 网络/API 异常 → 整体捕获,本轮不误标,留待下次。
  4. 电量(每天 1 次):每日首轮采样顺带读 battery-level → 写 MihomeDevice.battery/batteryAt(当天缓存,标题展示用,不频繁更新)。读不到不报错。
  5. 每日聚合(03:00 + 启动追赶): upsert MihomeEnvSample(deviceId, date):
    • temperatureAvg/humidityAvg = 当日各小时均值;temperatureMin/Max、 humidityMin/Max = 当日小时 min/max;temperature/humidity = 当日最后一次 live 值;
    • 当日采样小时数 < 8h 且设备在线的 → 判定硬件故障?否:仅当设备整天在线的阈值外仍缺, 视为设备离线缺档(保持缺口,不伪造)。
  6. 追赶同步(补齐):每次轮询/同步结束后检查——对每个设备 「缺小时档且采集入口健康」→ 按序补齐小时档;「今天就无样本」→ 立即执行每日聚合。 服务启动时触发一轮,实现停机恢复当天即补。
  7. 历史补齐(vendorHistoryFiller / vendorStats.js):每台设备先探测一次各 data_type 可用性(实测:炎米 ✅ / 青萍、VRF ❌-8 invalid params),仅对可用型号扫描近 45 天缺口:
    • 缺小时 → stat_hour_v3 回填 MihomeEnvHour.temperatureAvg/humidityAvg;
    • 缺天 → stat_day_v3 回填日档 min/max,再由小时档均值回填 avg;
    • 命中即写 origin=vendor_history;对「服务端停机跨天」缺口尤其有效。
  8. 缺口标记:GET /status 计算每设备最近 30 天缺样本日期列表(missingDays): 今天缺=待采;历史缺=先尝试 vendor 统计补齐,补不了则标注不可得。

补数边界(诚实策略):已开通统计的型号(炎米等)停机跨天缺口可用云端聚合真值补齐 ([min,max]/小时均值,origin=vendor_history);未开通的型号(青萍)保持缺口标注、不伪造。 「精确逐点历史」仍不可得,只能走局域网 BLE/网关(§10 可选项 C)。

5.4 路由表(均在 /api/mihome,authRequired)

方法 路径 说明
GET /status 授权状态 + 家庭/设备(含别名、电量、最新样本、30 天缺口、统计可用性探测结果);?force=1 跳过 5 分钟认证缓存强查一次
GET /device/:did/history?range=today 当天逐小时(见 §5.6);缺省 range=today
GET /device/:did/history?range=month 近 30 天逐日(日均 + min/max)
GET /device/:did/history?range=year 近 52 周逐周(周均值 + min/max,服务端按日档聚合)
PUT /device/:did { alias } 改别名(可清空)
POST /sync 手动触发一次采集(前端「立即同步」)
POST /reauthorize 开始扫码(返回二维码 URL)
GET /reauthorize/status 轮询扫码状态
POST /reauthorize/cancel 取消扫码

5.5 定时任务(并入 server/cron.js)

  • 0 * * * *(Asia/Shanghai):小时采样(温度+湿度,每小时整点 +5min 缓冲)。
  • 5 0,3 * * *:每日聚合 → battery 读取 → vendor 补齐扫描。
  • 兼容表达式:npm run data:mihome(手动跑一次采集器)备用。

5.6 历史查询聚合(history.js)

range 时间窗 粒度 数据源/算法
today 当天 00:00→now 小时 MihomeEnvHour 当日行;未来小时返回 null
month 近 30 天 天 MihomeEnvSample:temperatureAvg/humidityAvg(折线)+ min/max(区间带)
year 近 52 周 周 按日档现场聚合:每周 avg(日 avg 均值)、min(日 min 最小)、max(日 max 最大),不落库

响应统一形如 [{ t, temperature, temperatureMin, temperatureMax, humidity, humidityMin, humidityMax }], t 为桶起始(Asia/Shanghai),缺数据字段为 null。

6. 前端设计

src/pages/MonitorPage.jsx 的 tabs 增加 { key: 'env', label: '环境监控' }, 对应 src/pages/monitor/EnvView.jsx。

EnvView 结构

  1. 授权横幅(status !== ok): 米家授权已失效/待授权 + 【重新授权】按钮 → Popup 内 <img src=qrImageUrl> 3s 轮询 /reauthorize/status → done=true 自动关闭并刷新。
    • status=expired 且非网络类错误时,进入页面自动弹出一次扫码窗(用户关掉后不再重复弹)。
    • 二维码图片接口在 authRequired 后面,<img src> 带不上 JWT,必须用 fetch + Authorization 取二进制再转 blob: URL(见 src/api/mihome.js 的 fetchMihomeQrImage()),否则图片 401、弹窗里一片空白。
    • 认证探测不能只看 api.refreshToken():该库在本地凭证字段齐全时直接返回成功而不请求云端, 所以 checkAuth() 必须真打一次 getHomesList() 才能发现 30 天 token 过期。
  2. 顶部操作:立即同步(POST /sync)。
  3. 家庭列表:antd-mobile <Collapse accordion> 按家庭分组;组展开后显示该家庭 下的环境设备卡片;无环境设备的家庭 → 折叠标题旁标「无环境设备」占位。
  4. 设备卡片(不可折叠,点击进入详情/趋势):
    • 标题:别名(无别名则原名)+ 电量(如 卧室 78%,电量用 battery 当天缓存值, 不频繁请求);附带编辑别名按钮(Dialog.Prompt → PUT);
    • 概览:温度 28.1℃ · 湿度 71%(最新样本)+ 「缺 N 天」暗红角标;
    • 图表区:Segmented(antd-mobile) 切换 今天/月/年,默认今天; recharts LineChart 双线(温度/湿度)+ 可选 min/max 区间带(Area band):
      • 今天 → X 轴逐小时(00:00→当前),数据 /device/:did/history?range=today;
      • 月 → X 轴逐日(近 30 天),数据 ?range=month(日均折线 + 日 min/max 影带);
      • 年 → X 轴逐周(近 52 周),数据 ?range=year(周均值折线 + 周 min/max 影带);
    • 视图切换/拉数据留在卡片内部(不用全局 tab),逻辑沿用现有 ServerDetail.jsx / DataView.jsx 图表写法。
  5. 样式复用 globals.css 的 .card/.monitor-* 体系,新增少量 collapse 样式。

手风琴用 Collapse accordion;「不同时打开」即一次仅一个家庭展开。

7. 迁移与部署

  1. npm i @zythum02/mijia-api(加入 dependencies)。
  2. schema.prisma 追加 5 个模型 → npx prisma migrate dev --name add_mihome_env。
  3. .env 可选 MIHOME_CACHE_DIR(默认 server/mihome/.cache); 服务端放 .cache/ 于 .gitignore。
  4. 首次部署:在服务器上人工跑一次扫码授权(npm run data:mihome 的初始化提示 会输出二维码),写入服务器本地的 .cache/。
  5. 数据库迁移走现有 prisma migrate deploy 流程。

8. 风险与开放问题

# 风险/问题 应对
1 逐点历史不可读(设备本地日志),停机期要精确到刻的值补不上 已找到云端聚合通道(/v2/user/statistics,min/max/时均值,实测可行);按模型开通情况补齐(炎米 ✅ / 青萍 ❌);精确逐点 → 可选 §10 C
2 token 30 天过期需人工扫码 需求 4 的重新授权流程已覆盖;扫码流程 120s 超时自动换码
3 采样时刻固定凌晨,代表性一般 已改每小时采样,当天小时视图/日均/周均代表性充分;03:00 仅做日档聚合
4 第三方 @zythum02 库上游变动 锁定版本;模块边界隔离,只暴露我们自己需要的函数
5 VRF/其他设备写入延迟 5~6s 仅影响将来的控制功能,本模块只读,无影响

9. 实施步骤(里程碑)

  • M1 后端地基:依赖 + Prisma 迁移(5 模型)+ server/mihome (认证 + collector 小时采样/每日聚合/电量 + vendorStats 补齐 + history.js 三段聚合 + 路由)+ cron。
  • M2 前端:EnvView(授权弹窗 / 手风琴家庭组 / 设备卡=别名+电量 / 别名编辑 / Segmented 今天·月·年视图与图表 / 缺口角标)。
  • M3 联调:真机/Web 端到端 —— 小时采样与日聚合、停机恢复追赶与 vendor 补齐、 扫码重授权、别名、电量、三个粒度的图表与缺口展示。
  • M4(可选,已基本闭环):/v2/user/statistics 云端聚合历史通道已实测打通, 直接并入 M1 的 vendorHistoryFiller(无需再抓包 App)。
  • M5(可选):可选项 C 家侧本地采样代理(见 §10),用于「精确逐点」与分钟级采样。

10. 可选项 C:家侧本地采样代理 + 缓冲补数

背景:主线「云 API 每日采样」的停机跨天缺口现可由云端聚合统计(§2.2B)补齐到 [min,max]/小时 粒度;可选项 C 是把它做到精确逐点 / 分钟级——家侧一个常驻采集进程(可选配合小米中枢网关), 本地连续采样并缓冲,服务端恢复后拉真值回填。 与主线并行可选,不影响 §1~§9 落地;仅在追求细粒度历史时投入。

10.1 事实依据(已调研)

事实 结论
家庭现有网关 无中枢/多模网关;blt.* 设备挂在「小爱音箱 Play 增强版」上转发,该音箱不开放局域网 API
局域网直连设备本身 需本地 BLE 适配器;青萍闹钟(CGD1)协议已逆向但仅有实时/设置、无历史通道;炎米 ym001 零社区支持,需自行逆向厂商 GATT + 推导 MiKey,把握低
若有小米中枢网关 官方 HA 集成走网关内置本地 MQTT Broker,可本地化 WiFi+BLE 设备、断外网可用、延迟 10~50ms(社区实测 ha_xiaomi_home);但本地 Broker 只推实时 props,不提供历史
概率评估 本地实时读取:无网关走蓝牙 6085%(青萍高/炎米中);加中枢网关 ~85%。补停机期历史:三条局域网路线均不直接提供,只能靠「本地连续采样 + 缓冲」自建

10.2 方案

家庭内常驻小机(现有 Windows 机 / 树莓派) 跑 server/mihome-agent/
   ├─ 采样源(可切换): a) 云 getDevicesProp (同 §5.3, 无需新硬件)
   │                   b) 加中枢网关后走本地 MQTT(断外网可用, 秒级粒度)
   │                   c) 未来本地 BLE 嗅探(青萍 CGD1 优先)
   ├─ 每 N 分钟采样 → 写本地缓冲 SQLite/JSONL(本地保留 180 天)
   └─ 提供 GET /samples?from&to&did 供服务端拉取
服务端(iboard)
   └─ 停机恢复/每日同步时: 调 agent 拉缺口区间真实值 → 写 MihomeEnvSample(origin=backfill)
      (服务端主动拉 agent,agent 无需入公网,可用内网/隧道)

10.3 效果与代价

  • 效果:停机期 agent 缓冲持续采样,恢复后以精确逐点/分钟级真值回填(粒度高于 §2.2B 的 聚合统计);加中枢网关后数据链路断外网可用、秒级推送。
  • 代价:家中常驻进程(可复用现有 Windows 开发机);可选购买中枢网关(~¥349); agent 与服务端的传输通道需内网/VPN/隧道。
  • 决策点:是否投入属产品决策。当前「补齐停机缺口」已被 §2.2B 的云端统计覆盖(满足 「数据不比米家少」);C 只对「要更细粒度」有增量价值。接入点:agent 值写入 MihomeEnvSample,origin=backfill。
  • 本地直连协议现状(中枢/蓝牙):官方 HA 集成走中枢内置 MQTT Broker(需 HA 或移植其 MipsLocalClient 的证书鉴权+微链路,无公开协议,自有 Node agent 直连把握中低);纯 BLE 嗅探见 §10.1 表格(青萍 CGD1 有逆向基础、炎米零资料)。