import { InfluxDB } from '@influxdata/influxdb-client'; import { resolveRange } from '@/lib/ranges'; const URL = process.env.INFLUX_URL || 'http://nuccy:8086'; const TOKEN = process.env.INFLUX_TOKEN || ''; const ORG = process.env.INFLUX_ORG || 'citysensor'; const BUCKET = process.env.INFLUX_BUCKET || 'strom'; const MEASUREMENT = process.env.INFLUX_MEASUREMENT || 'vzlogger'; const POWER_FIELD = process.env.INFLUX_POWER_FIELD || 'leistung'; // momentane Leistung in W const TZ = process.env.INFLUX_TZ || 'Europe/Berlin'; // E-Auto-Ladung: Leistung laenger als MIN_HOURS ununterbrochen ueber THRESHOLD_KW export const LADEN_THRESHOLD_KW = parseFloat(process.env.LADEN_THRESHOLD_KW || '11'); export const LADEN_MIN_HOURS = parseFloat(process.env.LADEN_MIN_HOURS || '2'); const influx = new InfluxDB({ url: URL, token: TOKEN, timeout: 90_000 }); interface CacheEntry { days: number[]; cachedAt: number; } const cache = new Map(); const CURRENT_MONTH_TTL = 5 * 60 * 1000; // laufender Monat: 5 min; abgeschlossene Monate: dauerhaft function isCurrentMonth(month: string): boolean { const now = new Date(); const cur = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`; return month >= cur; } /** * Ermittelt die Tage eines Monats, an denen das E-Auto geladen wurde: * Leistung (`leistung`, W) lag laenger als LADEN_MIN_HOURS ununterbrochen ueber * LADEN_THRESHOLD_KW. Rueckgabe = Tages-Anfangs-Zeitstempel (ms, Berliner Tag), * passend zu den x-Werten der 31d-Balken. * * Erkennung serverseitig per Flux `stateDuration` (laeuft je Monat ~10-15s auf * den Sekundendaten), daher Caching: abgeschlossene Monate dauerhaft. */ export async function fetchLadetage(month: string): Promise { const thresholdW = LADEN_THRESHOLD_KW * 1000; const minSeconds = Math.round(LADEN_MIN_HOURS * 3600); const key = `${month}|${thresholdW}|${minSeconds}`; const hit = cache.get(key); if (hit && (!isCurrentMonth(month) || Date.now() - hit.cachedAt < CURRENT_MONTH_TTL)) { return hit.days; } const { start, stop } = resolveRange('31d', { month }); // 24h Vorlauf, damit ueber Mitternacht laufende Ladungen am Monatsanfang // korrekt erkannt werden (stateDuration startet sonst erst an der Range-Grenze). const queryStart = new Date(start.getTime() - 24 * 3600 * 1000); const flux = ` import "timezone" option location = timezone.location(name: "${TZ}") threshold = ${thresholdW}.0 from(bucket: "${BUCKET}") |> range(start: ${queryStart.toISOString()}, stop: ${stop.toISOString()}) |> filter(fn: (r) => r._measurement == "${MEASUREMENT}") |> filter(fn: (r) => r._field == "${POWER_FIELD}") |> stateDuration(fn: (r) => r._value > threshold, unit: 1s) |> filter(fn: (r) => r.stateDuration >= ${minSeconds}) |> aggregateWindow(every: 1d, fn: max, createEmpty: false) |> timeShift(duration: -1d) |> keep(columns: ["_time"]) `; const queryApi = influx.getQueryApi(ORG); const minTs = start.getTime(); const maxTs = stop.getTime(); const days: number[] = []; await new Promise((resolve, reject) => { queryApi.queryRows(flux, { next(row, tableMeta) { const o = tableMeta.toObject(row); const ts = new Date(o._time as string).getTime(); if (ts >= minTs && ts < maxTs) days.push(ts); }, error: reject, complete: resolve, }); }); cache.set(key, { days, cachedAt: Date.now() }); return days; }