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) {