142 lines
5.3 KiB
JavaScript
142 lines
5.3 KiB
JavaScript
// 历史查询聚合: today(时) / month(天) / year(周)
|
|
// 响应统一: [{ t(桶起始 ms, Asia/Shanghai), temperature, temperatureAvg, temperatureMin, temperatureMax,
|
|
// humidity, humidityAvg, humidityMin, humidityMax, sampleCount?, missing? }]
|
|
import { prisma } from '../db.js'
|
|
import { dayStart, hourStart, monthStart, yearStart, weekStart, addDays, addHours, cnDateStr } from './time.js'
|
|
|
|
// 设备是否存在 (路由层校验)
|
|
export async function getDevice(did) {
|
|
return prisma.mihomeDevice.findUnique({ where: { did } })
|
|
}
|
|
|
|
// ---------- 当天逐小时 ----------
|
|
export async function todayBuckets(device) {
|
|
const start = dayStart()
|
|
const now = hourStart()
|
|
const rows = await prisma.mihomeEnvHour.findMany({
|
|
where: { deviceId: device.id, hour: { gte: start, lt: addDays(start, 1) } },
|
|
orderBy: { hour: 'asc' },
|
|
})
|
|
const byHour = new Map(rows.map((r) => [r.hour.getTime(), r]))
|
|
const out = []
|
|
for (let t = start.getTime(); t <= now.getTime(); t += 3600000) {
|
|
const r = byHour.get(t)
|
|
out.push({
|
|
t,
|
|
temperature: r?.temperatureAvg ?? null,
|
|
humidity: r?.humidityAvg ?? null,
|
|
sampleCount: r ? 1 : 0,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ---------- 当前月逐日 ----------
|
|
export async function monthBuckets(device) {
|
|
const start = monthStart()
|
|
const today = dayStart()
|
|
const end = addDays(today, 1)
|
|
const rows = await prisma.mihomeEnvSample.findMany({
|
|
where: { deviceId: device.id, date: { gte: start, lt: end } },
|
|
orderBy: { date: 'asc' },
|
|
})
|
|
const byDay = new Map(rows.map((r) => [r.date.getTime(), r]))
|
|
const out = []
|
|
for (let t = start.getTime(); t < end.getTime(); t += 86400000) {
|
|
const r = byDay.get(t)
|
|
out.push(r
|
|
? {
|
|
t,
|
|
temperature: r.temperature,
|
|
temperatureAvg: r.temperatureAvg,
|
|
temperatureMin: r.temperatureMin,
|
|
temperatureMax: r.temperatureMax,
|
|
humidity: r.humidity,
|
|
humidityAvg: r.humidityAvg,
|
|
humidityMin: r.humidityMin,
|
|
humidityMax: r.humidityMax,
|
|
origin: r.origin,
|
|
sampleCount: 1,
|
|
}
|
|
: { t, sampleCount: 0, missing: true })
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ---------- 当前年逐周 (按日档现场聚合, 不落库) ----------
|
|
export async function yearBuckets(device) {
|
|
const start = yearStart()
|
|
const today = dayStart()
|
|
const end = addDays(today, 1)
|
|
const rows = await prisma.mihomeEnvSample.findMany({
|
|
where: { deviceId: device.id, date: { gte: start, lt: end } },
|
|
orderBy: { date: 'asc' },
|
|
})
|
|
// 日 -> 周 (上海周一为一周起点)
|
|
const weekOf = (dayStartMs) => weekStart(new Date(dayStartMs)).getTime()
|
|
const weeks = new Map() // weekStartMs -> { tempA: [], tempMin: [], tempMax: [], humA: [], humMin: [], humMax: [], days: [] }
|
|
for (const r of rows) {
|
|
const k = weekOf(r.date.getTime())
|
|
if (!weeks.has(k)) {
|
|
weeks.set(k, { tempA: [], tempMin: [], tempMax: [], humA: [], humMin: [], humMax: [], days: [] })
|
|
}
|
|
const w = weeks.get(k)
|
|
if (r.temperatureAvg != null) w.tempA.push(r.temperatureAvg)
|
|
if (r.temperatureMin != null) w.tempMin.push(r.temperatureMin)
|
|
if (r.temperatureMax != null) w.tempMax.push(r.temperatureMax)
|
|
if (r.humidityAvg != null) w.humA.push(r.humidityAvg)
|
|
if (r.humidityMin != null) w.humMin.push(r.humidityMin)
|
|
if (r.humidityMax != null) w.humMax.push(r.humidityMax)
|
|
w.days.push(r.date)
|
|
}
|
|
const avg = (a) => (a.length ? a.reduce((x, y) => x + y, 0) / a.length : null)
|
|
const out = []
|
|
for (const [k, w] of [...weeks.entries()].sort((a, b) => a[0] - b[0])) {
|
|
// 补齐周内缺的天数 (统计无数据的日算缺失)
|
|
const wkStart = new Date(k)
|
|
const missing = addDays(wkStart, 7)
|
|
const haveMap = new Set(w.days.map((d) => d.getTime()))
|
|
let missingDays = 0
|
|
for (let t = k; t < missing.getTime(); t += 86400000) {
|
|
// 只统计当年/到今天的日
|
|
if (t >= start.getTime() && t < end.getTime() && !haveMap.has(t)) missingDays++
|
|
}
|
|
out.push({
|
|
t: k,
|
|
label: cnDateStr(wkStart).slice(5) + '~' + cnDateStr(addDays(wkStart, 6)).slice(5),
|
|
temperatureAvg: avg(w.tempA),
|
|
temperatureMin: w.tempMin.length ? Math.min(...w.tempMin) : null,
|
|
temperatureMax: w.tempMax.length ? Math.max(...w.tempMax) : null,
|
|
humidityAvg: avg(w.humA),
|
|
humidityMin: w.humMin.length ? Math.min(...w.humMin) : null,
|
|
humidityMax: w.humMax.length ? Math.max(...w.humMax) : null,
|
|
sampleCount: w.days.length,
|
|
missingDays,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ---------- 设备概览 (状态页用): 最新样本 + 近 30 天缺日列表 ----------
|
|
export async function deviceOverview(device) {
|
|
const latest = await prisma.mihomeEnvSample.findFirst({
|
|
where: { deviceId: device.id },
|
|
orderBy: { date: 'desc' },
|
|
})
|
|
// 近 30 天 (含今天) 缺少样本的上海日
|
|
const start = addDays(dayStart(), -29)
|
|
const end = addDays(dayStart(), 1)
|
|
const have = await prisma.mihomeEnvSample.findMany({
|
|
where: { deviceId: device.id, date: { gte: start, lt: end } },
|
|
select: { date: true },
|
|
})
|
|
const haveSet = new Set(have.map((h) => h.date.getTime()))
|
|
const missingDays = []
|
|
for (let t = start.getTime(); t < end.getTime(); t += 86400000) {
|
|
if (!haveSet.has(t)) missingDays.push(cnDateStr(new Date(t)))
|
|
}
|
|
return {
|
|
latest,
|
|
missingDays,
|
|
}
|
|
} |