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:
2026-06-28 14:49:44 +02:00
parent 3649bf4e22
commit 46c8057155
6 changed files with 218 additions and 10 deletions
+20 -2
View File
@@ -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<number>;
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,
},
],
+63 -8
View File
@@ -50,6 +50,32 @@ function ChartCard({ range, every, title, color, selector }: ChartCardProps) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// E-Auto-Ladetage (nur 31d): werden asynchron nachgeladen
const [ladeDays, setLadeDays] = useState<Set<number>>(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<number>(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) {
</div>
<button
onClick={() => fetchData(value)}
onClick={handleApply}
disabled={isLoading}
className="bg-[#85B7D7] hover:bg-[#6a9fc5] px-5 py-2 text-sm rounded-lg transition-colors shadow-md disabled:opacity-50"
>
@@ -156,13 +188,36 @@ function ChartCard({ range, every, title, color, selector }: ChartCardProps) {
<div className="text-center py-16 text-gray-500">Lade Daten</div>
) : (
<>
<BarChart title={title} data={data} every={every} color={color} xMin={xMin} xMax={xMax} />
{data.length > 0 && (
<div className="text-right text-sm font-semibold mt-2 px-2">
{totalLabel}:{' '}
{total.toLocaleString('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 1 })} kWh
</div>
)}
<BarChart
title={title}
data={data}
every={every}
color={color}
xMin={xMin}
xMax={xMax}
highlight={range === '31d' ? ladeDays : undefined}
/>
<div className="flex justify-between items-center mt-2 px-2 text-sm">
{/* Legende: E-Auto-Ladetage (nur 31d) */}
{range === '31d' ? (
<span className="text-gray-600 flex items-center gap-1.5">
<span className="inline-block w-3 h-3 rounded-sm" style={{ backgroundColor: '#c0392b' }} />
{ladeLoading
? 'E-Auto-Ladung wird geprüft…'
: ladeDays.size > 0
? `E-Auto geladen (> ${ladeMeta?.thresholdKw ?? ''} kW, ≥ ${ladeMeta?.minHours ?? ''} h)`
: 'Keine E-Auto-Ladung erkannt'}
</span>
) : (
<span />
)}
{data.length > 0 && (
<span className="font-semibold whitespace-nowrap">
{totalLabel}:{' '}
{total.toLocaleString('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 1 })} kWh
</span>
)}
</div>
</>
)}
</div>