From 46c8057155cba58acd3a61b6413f0422e4e2566a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reinhard=20X=2E=20F=C3=BCrst?= Date: Sun, 28 Jun 2026 14:49:44 +0200 Subject: [PATCH] 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) --- .env.local.example | 6 +++ README.md | 10 ++++ app/api/laden/route.ts | 28 +++++++++++ components/BarChart.tsx | 22 +++++++- components/VerbrauchCharts.tsx | 71 +++++++++++++++++++++++--- lib/laden.ts | 91 ++++++++++++++++++++++++++++++++++ 6 files changed, 218 insertions(+), 10 deletions(-) create mode 100644 app/api/laden/route.ts create mode 100644 lib/laden.ts diff --git a/.env.local.example b/.env.local.example index 33b17a9..5ba8031 100644 --- a/.env.local.example +++ b/.env.local.example @@ -11,3 +11,9 @@ INFLUX_ROLLUP_MEASUREMENT=arbeit_hourly INFLUX_FIELD=arbeit # Umrechnung auf kWh: Zaehlerstand liegt in Wh vor -> 0.001 INFLUX_UNIT_FACTOR=0.001 + +# E-Auto-Lade-Erkennung (31d-Diagramm): Leistung (W) laenger als +# LADEN_MIN_HOURS ununterbrochen ueber LADEN_THRESHOLD_KW -> Tag markiert +INFLUX_POWER_FIELD=leistung +LADEN_THRESHOLD_KW=11 +LADEN_MIN_HOURS=2 diff --git a/README.md b/README.md index 062af1f..3caeab4 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,16 @@ from(bucket:"strom") X-Achse: Datum/Zeit · Y-Achse: kWh +### E-Auto-Lade-Erkennung (31d) + +Im 31d-Diagramm werden Tage **rot** markiert, an denen das E-Auto geladen wurde: +Die Leistung (Feld `leistung`, W) lag **länger als `LADEN_MIN_HOURS` (Default 2 h) +ununterbrochen über `LADEN_THRESHOLD_KW` (Default 11 kW)** — dem Anschlusswert des +Autos. Erkennung serverseitig per Flux `stateDuration` auf den Sekundendaten +(`/api/laden?month=YYYY-MM`); läuft ~15 s je Monat und wird daher gecacht +(abgeschlossene Monate dauerhaft). Die Markierung wird asynchron nachgeladen, +sodass die Balken sofort erscheinen. Schwelle/Dauer sind per Env einstellbar. + ## Setup ```bash diff --git a/app/api/laden/route.ts b/app/api/laden/route.ts new file mode 100644 index 0000000..6b73235 --- /dev/null +++ b/app/api/laden/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { fetchLadetage, LADEN_THRESHOLD_KW, LADEN_MIN_HOURS } from '@/lib/laden'; + +// GET /api/laden?month=YYYY-MM +// Liefert die Tage (Tages-Anfangs-Zeitstempel ms), an denen das E-Auto geladen wurde. +export async function GET(request: NextRequest) { + const month = request.nextUrl.searchParams.get('month'); + if (!month || !/^\d{4}-\d{2}$/.test(month)) { + return NextResponse.json( + { success: false, error: 'Parameter month=YYYY-MM erforderlich.' }, + { status: 400 } + ); + } + + try { + const days = await fetchLadetage(month); + return NextResponse.json( + { success: true, days, thresholdKw: LADEN_THRESHOLD_KW, minHours: LADEN_MIN_HOURS }, + { headers: { 'Cache-Control': 'no-store' } } + ); + } catch (error) { + console.error('Laden detection error:', error); + return NextResponse.json( + { success: false, error: 'Fehler bei der Lade-Erkennung.' }, + { status: 500 } + ); + } +} diff --git a/components/BarChart.tsx b/components/BarChart.tsx index 76e1a48..a3a658c 100644 --- a/components/BarChart.tsx +++ b/components/BarChart.tsx @@ -13,12 +13,24 @@ interface BarChartProps { // Optionale feste Achsengrenzen (z.B. komplettes Jahr -> immer 12 Monate sichtbar) xMin?: number; xMax?: number; + // Hervorzuhebende Balken (Zeitstempel-Set, z.B. E-Auto-Ladetage) + Farbe + highlight?: Set; + highlightColor?: string; } const HOUR_MS = 60 * 60 * 1000; const DAY_MS = 24 * HOUR_MS; -export default function BarChart({ title, data, every, color, xMin, xMax }: BarChartProps) { +export default function BarChart({ + title, + data, + every, + color, + xMin, + xMax, + highlight, + highlightColor = '#c0392b', +}: BarChartProps) { const isHourly = every === '1h'; const isMonthly = every === '1mo'; const tooltipDateFormat = isHourly ? '%d.%m.%Y %H:%M' : isMonthly ? '%B %Y' : '%d.%m.%Y'; @@ -76,7 +88,13 @@ export default function BarChart({ title, data, every, color, xMin, xMax }: BarC { type: 'column', name: 'Verbrauch', - data, + // markierte Balken (z.B. Ladetage) bekommen eine eigene Farbe + data: + highlight && highlight.size + ? data.map(([x, y]) => + highlight.has(x) ? { x, y, color: highlightColor } : { x, y } + ) + : data, color, }, ], diff --git a/components/VerbrauchCharts.tsx b/components/VerbrauchCharts.tsx index c17fd88..b8fdfac 100644 --- a/components/VerbrauchCharts.tsx +++ b/components/VerbrauchCharts.tsx @@ -50,6 +50,32 @@ function ChartCard({ range, every, title, color, selector }: ChartCardProps) { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); + // E-Auto-Ladetage (nur 31d): werden asynchron nachgeladen + const [ladeDays, setLadeDays] = useState>(new Set()); + const [ladeLoading, setLadeLoading] = useState(false); + const [ladeMeta, setLadeMeta] = useState<{ thresholdKw: number; minHours: number } | null>(null); + + const fetchLaden = useCallback( + async (sel: string) => { + if (range !== '31d') return; + setLadeLoading(true); + setLadeDays(new Set()); + try { + const res = await fetch(`/api/laden?month=${sel}`, { cache: 'no-store' }); + const json = await res.json(); + if (json.success) { + setLadeDays(new Set(json.days)); + setLadeMeta({ thresholdKw: json.thresholdKw, minHours: json.minHours }); + } + } catch { + // Markierung ist sekundaer -> Fehler still ignorieren + } finally { + setLadeLoading(false); + } + }, + [range] + ); + const fetchData = useCallback( async (sel: string) => { setIsLoading(true); @@ -83,9 +109,15 @@ function ChartCard({ range, every, title, color, selector }: ChartCardProps) { useEffect(() => { fetchData(value); + fetchLaden(value); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const handleApply = () => { + fetchData(value); + fetchLaden(value); + }; + // Jahres-Chart: Achse immer ueber das komplette Kalenderjahr -> alle 12 Monate // sichtbar, Restmonate bleiben ohne Balken. let xMin: number | undefined; @@ -138,7 +170,7 @@ function ChartCard({ range, every, title, color, selector }: ChartCardProps) {