46c8057155
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>
92 lines
3.4 KiB
TypeScript
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;
|
|
}
|