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)与接口清单
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
# 环境监控模块设计(米家温湿度长期记录)
|
||||
|
||||
> 目标项目: 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 有逆向基础、炎米零资料)。
|
||||
Reference in New Issue
Block a user