新增米家环境监控模块 (采集/聚合/历史/前端 EnvView) + CI 挂载米家凭证目录并自动应用迁移
Docker Build and Push / build-image (push) Successful in 6m54s
Docker Build and Push / build-image (push) Successful in 6m54s
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
// 环境监控 REST 路由: /api/mihome/* (authRequired)
|
||||
import { Router } from 'express'
|
||||
import { prisma } from '../db.js'
|
||||
import { authRequired } from '../middleware.js'
|
||||
import {
|
||||
getStatus,
|
||||
getDevice,
|
||||
todayBuckets,
|
||||
monthBuckets,
|
||||
yearBuckets,
|
||||
syncNow,
|
||||
startQrLogin,
|
||||
cancelQrLogin,
|
||||
getQrState,
|
||||
getQrImageUrl,
|
||||
MihomeAuthError,
|
||||
} from '../mihome/index.js'
|
||||
import { XIAOMI_APP_UA } from '../mihome/specs.js'
|
||||
|
||||
const router = Router()
|
||||
router.use(authRequired)
|
||||
|
||||
// 统一错误边界: 米家授权错误映射为 401, 其余走 500
|
||||
function guard(handler) {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
return await handler(req, res, next)
|
||||
} catch (e) {
|
||||
if (e instanceof MihomeAuthError) return res.status(401).json({ error: e.message })
|
||||
return next(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 授权状态 + 家庭/设备 + 概览 (电量/最新样本/缺失天数)
|
||||
router.get('/status', guard(async (_req, res) => {
|
||||
res.json(await getStatus())
|
||||
}))
|
||||
|
||||
// 历史: range = today(默认) | month | year
|
||||
router.get('/device/:did/history', guard(async (req, res) => {
|
||||
const device = await getDevice(req.params.did)
|
||||
if (!device) return res.status(404).json({ error: '设备不存在' })
|
||||
const range = req.query.range || 'today'
|
||||
let buckets
|
||||
if (range === 'month') buckets = await monthBuckets(device)
|
||||
else if (range === 'year') buckets = await yearBuckets(device)
|
||||
else buckets = await todayBuckets(device)
|
||||
res.json({ did: device.did, range: range === 'today' ? 'today' : range, buckets })
|
||||
}))
|
||||
|
||||
// 改别名 (可清空); alias 最长 24 字符
|
||||
router.put('/device/:did', guard(async (req, res) => {
|
||||
const device = await getDevice(req.params.did)
|
||||
if (!device) return res.status(404).json({ error: '设备不存在' })
|
||||
const alias = typeof req.body?.alias === 'string' ? req.body.alias.trim().slice(0, 24) : ''
|
||||
const updated = await prisma.mihomeDevice.update({ where: { id: device.id }, data: { alias: alias || null } })
|
||||
res.json({ did: updated.did, alias: updated.alias || '' })
|
||||
}))
|
||||
|
||||
// 立即同步一次 (小时采样 + 日聚合), 首页「立即同步」
|
||||
router.post('/sync', guard(async (_req, res) => {
|
||||
res.json(await syncNow())
|
||||
}))
|
||||
|
||||
// 重授权: 启动扫码 (幂等; 已在跑则返回当前状态)
|
||||
router.post('/reauthorize', guard(async (_req, res) => {
|
||||
res.json(await startQrLogin())
|
||||
}))
|
||||
|
||||
// 扫码状态轮询
|
||||
router.get('/reauthorize/status', guard(async (_req, res) => {
|
||||
res.json(getQrState())
|
||||
}))
|
||||
|
||||
// 取消扫码
|
||||
router.post('/reauthorize/cancel', guard(async (_req, res) => {
|
||||
res.json(cancelQrLogin())
|
||||
}))
|
||||
|
||||
// 代理二维码图片 (带小米 UA, 避免被 CDN 拒)
|
||||
router.get('/reauthorize/qr-image', guard(async (_req, res) => {
|
||||
const url = getQrImageUrl()
|
||||
if (!url) return res.status(404).json({ error: '暂无二维码' })
|
||||
const r = await fetch(url, { headers: { 'User-Agent': XIAOMI_APP_UA } })
|
||||
if (!r.ok) throw new Error('二维码拉取失败 ' + r.status)
|
||||
const buf = Buffer.from(await r.arrayBuffer())
|
||||
res.set('Content-Type', r.headers.get('content-type') || 'image/png')
|
||||
res.set('Cache-Control', 'no-store')
|
||||
res.send(buf)
|
||||
}))
|
||||
|
||||
export default router
|
||||
Reference in New Issue
Block a user