Files
strom-next/lib/laden.ts
T
admin 46c8057155 E-Auto-Lade-Erkennung im 31d-Diagramm
Markiert im 31d-Diagramm die Tage rot, an denen das E-Auto geladen wurde:
Leistung (Feld 'leistung', W) lag laenger als LADEN_MIN_HOURS (Default 2h)
ununterbrochen ueber LADEN_THRESHOLD_KW (Default 11 kW).

- Erkennung serverseitig per Flux stateDuration auf den Sekundendaten
  (24h Vorlauf fuer ueber Mitternacht laufende Ladungen an der Monatsgrenze)
- /api/laden?month=YYYY-MM, Ergebnis gecacht (abgeschlossene Monate dauerhaft,
  laufender Monat 5 min) -> ~15s nur beim ersten Aufruf
- Markierung wird asynchron nachgeladen, Balken erscheinen sofort
- Schwelle/Dauer per Env (LADEN_THRESHOLD_KW, LADEN_MIN_HOURS) einstellbar
- BarChart: optionales highlight-Set faerbt passende Balken

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 14:49:44 +02:00

92 lines
3.4 KiB
TypeScript

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<string, CacheEntry>();
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<number[]> {
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<void>((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;
}