Files
iboard/server/mihome/vendorStats.js
T

179 lines
6.0 KiB
JavaScript

// /v2/user/statistics 云端聚合历史: 探测可用性 + 小时/日缺口回填
// 实测: 炎米 ym001 可用 (stat_hour_v3/stat_day_v3/stat_month_v3); 青萍/VRF 返回 -8 invalid params
import { prisma } from '../db.js'
import { getApi } from './client.js'
import { dayStart, hourStart, addDays, addHours } from './time.js'
import { getSpec } from './specs.js'
// 型号 -> 是否支持统计 (探测一次后缓存; undefined=未探测)
const capabilityCache = new Map()
export function capabilityOf(model) {
if (capabilityCache.has(model)) return capabilityCache.get(model)
return undefined
}
// 用同型号任一设备探测该型号统计可用性
async function ensureCapability(device) {
if (capabilityCache.has(device.model)) return capabilityCache.get(device.model)
let supported = false
try {
const api = getApi()
const res = await api.getStatistics({
did: device.did,
key: `${device.tempSiid}.${device.tempPiid}`,
data_type: 'stat_day_v3',
limit: 1,
time_start: Math.floor(Date.now() / 1000) - 86400,
time_end: Math.floor(Date.now() / 1000),
})
supported = Array.isArray(res)
} catch (e) {
supported = false
}
capabilityCache.set(device.model, supported)
console.log(`[mihome] 统计探测 ${device.model}: ${supported ? '可用' : '不可用'}`)
return supported
}
// 解析 "[28.8]" / "[28.1,29.200001]" -> [28.8, 29.2]
function parseStatValue(value) {
if (typeof value !== 'string') return []
const m = value.match(/\[([^\]]*)\]/)
if (!m) return []
return m[1]
.split(',')
.map((s) => parseFloat(s.trim()))
.filter((n) => Number.isFinite(n))
}
// 拉某设备某属性的小时均值序列 (stat_hour_v3), 默认带排序
async function fetchStatHours(device, key, fromTs, toTs) {
const api = getApi()
const res = await api.getStatistics({
did: device.did,
key,
data_type: 'stat_hour_v3',
limit: 2000,
time_start: fromTs,
time_end: toTs,
})
if (!Array.isArray(res)) return []
return res
.filter((r) => r && r.time)
.sort((a, b) => a.time - b.time)
.map((r) => ({ time: new Date(r.time * 1000), values: parseStatValue(r.value) }))
}
// 回填小时缺口: 查 [since, now) 内缺失的整点桶, 从云端补齐 (origin 标记 raw.vendor=true)
// 返回新增小时桶数量
export async function fillHourGaps(device, since) {
if (!(await ensureCapability(device))) return 0
const now = hourStart()
if (since >= now) return 0
const existing = await prisma.mihomeEnvHour.findMany({
where: {
deviceId: device.id,
hour: { gte: since, lte: addHours(now, -1) },
},
select: { hour: true },
})
const have = new Set(existing.map((h) => h.hour.getTime()))
// 实际缺失桶数
let missingCount = 0
for (let t = since.getTime(); t <= addHours(now, -1).getTime(); t += 3600000) {
if (!have.has(t)) missingCount++
}
if (missingCount === 0) return 0
const fromTs = Math.floor(since.getTime() / 1000)
const toTs = Math.floor(now.getTime() / 1000)
let filled = 0
for (const [siid, piid, col, num] of [
[device.tempSiid, device.tempPiid, 'temperatureAvg', 0],
[device.humSiid, device.humPiid, 'humidityAvg', 1],
]) {
const rows = await fetchStatHours(device, `${siid}.${piid}`, fromTs, toTs)
for (const r of rows) {
const bucket = r.time.getTime()
if (r.values.length <= num || have.has(bucket) || bucket > now.getTime()) continue
// 只补严格已过去的整点
have.add(bucket)
try {
await prisma.mihomeEnvHour.upsert({
where: { deviceId_hour: { deviceId: device.id, hour: r.time } },
create: { deviceId: device.id, hour: r.time, [col]: r.values[num], raw: { vendor: true } },
update: { [col]: r.values[num], raw: { vendor: true } },
})
filled++
} catch (e) {
if (e?.code !== 'P2002') throw e
}
}
}
if (filled > 0) console.log(`[mihome] vendor 补齐小时档 ${device.did}: +${filled}`)
return filled
}
// 回填日缺口: 对仍无日档的日子, 若云端有日 [min,max] 则补一条 vendor_history
export async function fillDayGaps(device, since) {
if (!(await ensureCapability(device))) return 0
const today = dayStart()
const existingSamples = await prisma.mihomeEnvSample.findMany({
where: { deviceId: device.id, date: { gte: since, lt: today } },
select: { date: true },
})
const haveDay = new Set(existingSamples.map((s) => s.date.getTime()))
const fromTs = Math.floor(since.getTime() / 1000)
const toTs = Math.floor(Date.now() / 1000)
let filled = 0
for (const [siid, piid] of [
[device.tempSiid, device.tempPiid],
[device.humSiid, device.humPiid],
]) {
const api = getApi()
const res = await api.getStatistics({
did: device.did,
key: `${siid}.${piid}`,
data_type: 'stat_day_v3',
limit: 120,
time_start: fromTs,
time_end: toTs,
})
if (!Array.isArray(res)) continue
const isTemp = siid === device.tempSiid && piid === device.tempPiid
for (const r of res) {
if (!r || !r.time) continue
const day = new Date(r.time * 1000)
const vals = parseStatValue(r.value)
const bucket = dayStart(day.getTime()).getTime()
if (haveDay.has(bucket) || bucket >= today.getTime() || vals.length < 2) continue
haveDay.add(bucket)
const data = isTemp
? { temperatureMin: vals[0], temperatureMax: vals[1] }
: { humidityMin: vals[0], humidityMax: vals[1] }
try {
await prisma.mihomeEnvSample.upsert({
where: { deviceId_date: { deviceId: device.id, date: dayStart(day.getTime()) } },
create: {
deviceId: device.id,
date: dayStart(day.getTime()),
origin: 'vendor_history',
...data,
},
update: { origin: 'vendor_history', ...data },
})
filled++
} catch (e) {
if (e?.code !== 'P2002') throw e
}
}
}
if (filled > 0) console.log(`[mihome] vendor 补齐日档 ${device.did}: +${filled}`)
return filled
}