Files
iboard/docs/V0.1/环境监控模块设计.md
T
cheney 004f8c262d feat(V0.2): 打卡 / 日历 / 清单模块
- 数据: Habit/CheckIn/CheckList/CheckListItem/CheckListTemplate + 迁移 + 重新生成 client
- 后端: server/checkin/period.js 周期计算(日/周/月/年, 周一起始, Asia/Shanghai)
        /api/checkin/* 打卡项CRUD/打卡/撤销/补打/某日详情/月历
        /api/checklists/* 清单与检查项CRUD/勾选/全选/复制/模板(系统预置出差·回老家)
- 前端: 底部新增「打卡」Tab -> /checkin, 页内三视图
        环形进度 RingProgress + 完成特效(粒子/光晕/触感, 可关闭)
        打卡视图: 待办列表/乐观更新/撤销/新建编辑
        日历视图: 月历网格 + 今天·昨天·明天 + 日期标记/完成率
        清单视图: 场景清单/勾选进度条/模板生成/另存模板/归档
- 文档: docs/V0.2/需求文档.md 补充实现约定(附录B)与接口清单
2026-10-22 11:37:16 +08:00

392 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 环境监控模块设计(米家温湿度长期记录)
> 目标项目: 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)
```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 认证与「重新授权」
- 启动时 `login()` 自动尝试加载/刷新 `DiskCache` 凭证;成功则 `status=ok`。
- 失败(30 天过期 / 风控)→ `MihomeAccount.status=expired`,前端弹授权横幅。
- 重新授权流程(服务端驱动,前端轮询):
```
POST /api/mihome/reauthorize
└─ 若 pendingQr 空闲 => 后台启动 QRlogin 循环(每轮 120s 超时自动续二维码)
└─ 立即返回 { state, qrImageUrl }
GET /api/mihome/reauthorize/status
└─ 返回 { state: idle|waiting|scanning|ok|failed, qrImageUrl?, lastError? }
└─ 扫码成功(qrlogin 返回) => 保存凭证 => 状态 ok => 触发一次立即同步
POST /api/mihome/reauthorize/cancel # 人工取消, 停掉后台循环
```
- `pendingQr` 为模块级单例,防并发(同一时间只允许一个扫码流程)。
- 前端在弹窗内 `setInterval 3s` 轮询直到 `ok`,成功后关弹窗并刷新列表。
### 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 天缺口、统计可用性探测结果) |
| 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` → 成功自动关闭并刷新。
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,不提供历史** |
| 概率评估 | 本地实时读取:无网关走蓝牙 ~60~85%(青萍高/炎米中);加中枢网关 ~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 有逆向基础、炎米零资料)。