// 环境监控 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) } } } // 授权状态 + 家庭/设备 + 概览 (电量/最新样本/缺失天数) // force=1: 跳过后端 5 分钟认证缓存强查 (前端打开页面时用, 保证会话失效能立刻发现) router.get('/status', guard(async (req, res) => { res.json(await getStatus(req.query.force === '1')) })) // 历史: 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