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>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user