Solar: neue Energiefluss-Uebersicht, anpassbar je Haus

Das feste realtime.svg ist ersetzt durch einen Katalog
(js/solar/energieflussKatalog.js: jeder Kreis einmal, samt Werten aus MQTT)
und einen Renderer (js/solar/energiefluss.js): Ringe mit Segmenten,
Fluesse als Boegen mit laufenden Punkten, Fuellstandswelle fuer Batterie
und Auto, animierte Symbole, Plaketten, Hochkant-Ansicht auf schmalen
Schirmen. Hierarchie und Lage wie bisher: Quellen -> Wechselrichter ->
Verbrauch -> OG/EG/UG -> Wallboxen, Heizstab.

Einstellungen -> Uebersicht: Kreise ein/aus (Kinder ruecken eine Ebene
hoeher), Namen, Lage per Ziehen in der Vorschau, PV-Flaechen. Gespeichert
werden nur Abweichungen in homeMesh.energiefluss.

Der PV-Lageplan hinter der Sonne liegt unveraendert als assets/img/pvplan.svg
und ist ueber den Umschalter Uebersicht/Anlage erreichbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 16:42:52 +02:00
co-authored by Claude Opus 5
parent 5ed3a0796e
commit a59277e33e
14 changed files with 1812 additions and 1148 deletions
+170
View File
@@ -3431,3 +3431,173 @@ input[type='range']::-webkit-slider-runnable-track {
border-color: #00788F;
color: var(--ton-schrift, #e8ede9);
}
/* ==========================================================================
Energiefluss-Übersicht (js/solar/energiefluss.js, Katalog in
js/solar/energieflussKatalog.js).
Farben je Kreis kommen als --farbe aus dem Katalog, Breite und Tempo der
Flüsse als --breite/--dauer aus dem Renderer. Alles Übrige steht hier.
Transformationen liegen nie auf Elementen mit transform-Attribut (Kreis-
gruppe, Plakette) - CSS würde deren Lage überschreiben.
========================================================================== */
.enf-behaelter { position: relative; }
svg.enf {
display: block; width: 100%; height: auto; max-height: 80vh;
font-family: "Poppins", system-ui, sans-serif;
user-select: none; -webkit-tap-highlight-color: transparent;
}
svg.enf.enf-hoch { max-height: none; }
/* --- Kreis ------------------------------------------------------------- */
.enf .enf-hof { fill: #1c2024; stroke: rgba(255, 255, 255, .06); stroke-width: 1.5; transition: stroke .3s, stroke-width .3s; }
.enf .enf-spur { fill: none; stroke: #2b3139; stroke-width: 14; }
.enf .enf-bogen {
fill: none; stroke-width: 14; stroke-dasharray: 0 100;
transition: stroke-dasharray 1s cubic-bezier(.2, .7, .2, 1), stroke-dashoffset 1s cubic-bezier(.2, .7, .2, 1), stroke .4s;
}
.enf .enf-scheibe { fill: #20252b; }
.enf .enf-wert { fill: var(--ton-schrift, #e8ede9); font-size: 32px; font-weight: 600; text-anchor: middle; font-variant-numeric: tabular-nums; }
.enf .enf-neben { fill: var(--ton-schrift-2, #9aa6ad); font-size: 14.5px; text-anchor: middle; font-variant-numeric: tabular-nums; }
.enf .enf-name {
fill: var(--ton-schrift-2, #9aa6ad); font-size: 16px; font-weight: 600; text-anchor: middle; letter-spacing: .08em; text-transform: uppercase;
/* Kontur in Kartenfarbe: bleibt lesbar, wo ein Fluss unter dem Namen durchlaeuft. */
paint-order: stroke; stroke: var(--ton-flaeche, #23282c); stroke-width: 7px; stroke-linejoin: round;
}
.enf .enf-koerper { transform-box: view-box; transform-origin: 0 0; transition: transform .3s cubic-bezier(.2, .7, .2, 1), opacity .6s; }
.enf .enf-ruhe .enf-koerper { opacity: .6; }
.enf .enf-ruhe .enf-name { opacity: .7; }
.enf .enf-aus { opacity: .3; }
.enf .enf-aus .enf-spur { stroke-dasharray: 8 8; }
.enf .enf-klickbar { cursor: pointer; outline: none; }
.enf .enf-klickbar:hover .enf-koerper { transform: scale(1.045); }
.enf .enf-klickbar:hover .enf-hof,
.enf .enf-klickbar:focus-visible .enf-hof { stroke: var(--farbe); stroke-width: 3; }
.enf .enf-tipp circle { fill: #262c33; stroke: #3a424b; stroke-width: 1.5; }
.enf .enf-tipp path { fill: none; stroke: var(--farbe); stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round; transition: transform .25s; }
.enf .enf-klickbar:hover .enf-tipp path { transform: translateX(2px); }
/* --- Füllstand als Welle ------------------------------------------------ */
.enf .enf-fuellung {
--r: 67;
transform: translateY(calc(var(--r) * (1 - 2 * var(--stand)) * 1px));
transition: transform 1.4s cubic-bezier(.2, .7, .2, 1);
}
.enf .enf-welle { fill: var(--farbe); fill-opacity: .14; animation: enf-wogen 8s linear infinite; }
@keyframes enf-wogen { to { transform: translateX(calc(var(--r) * 2px)); } }
/* --- Symbole ------------------------------------------------------------ */
.enf .enf-symbol { color: var(--farbe); fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.enf .enf-s-fein { stroke-width: 1.1; opacity: .75; }
.enf .enf-s-kern { fill: currentColor; fill-opacity: .2; transition: fill-opacity .6s; }
.enf .enf-an .enf-s-kern { fill-opacity: .55; }
.enf .enf-s-strahlen { transform-box: view-box; transform-origin: 0 0; opacity: .55; transition: opacity .6s; }
.enf .enf-an .enf-s-strahlen { opacity: 1; animation: enf-drehen 30s linear infinite; }
@keyframes enf-drehen { to { transform: rotate(360deg); } }
.enf .enf-s-stand {
fill: currentColor; stroke: none; opacity: .45;
transform-box: fill-box; transform-origin: 50% 100%;
transform: scaleY(var(--stand, 0)); transition: transform 1.2s cubic-bezier(.2, .7, .2, 1);
}
.enf .enf-s-blitz { fill: currentColor; stroke: none; opacity: 0; transition: opacity .4s; }
.enf .enf-sym-akku .enf-s-blitz { fill: #fff; }
.enf .enf-s-blitz.enf-s-an,
.enf .enf-laden .enf-s-blitz,
.enf .enf-an.enf-knoten .enf-sym-wallbox .enf-s-blitz { opacity: 1; }
.enf .enf-laden .enf-s-blitz,
.enf .enf-an .enf-sym-wallbox .enf-s-blitz { animation: enf-puls 1.8s ease-in-out infinite; }
@keyframes enf-puls { 50% { opacity: .35; } }
.enf .enf-an .enf-s-welle { animation: enf-puls 1.4s ease-in-out infinite; }
.enf .enf-s-ebene { fill: currentColor; stroke: none; opacity: .38; }
.enf .enf-an .enf-sym-wallbox .enf-s-kabel { stroke-dasharray: 2.4 2.4; animation: enf-kabel .9s linear infinite; }
@keyframes enf-kabel { to { stroke-dashoffset: -4.8; } }
.enf .enf-s-temps rect { stroke: none; fill: #2b3139; opacity: .9; transition: fill 1.5s; }
.enf .enf-s-spirale { stroke-width: 1.3; transition: stroke .4s; }
.enf .enf-an .enf-s-spirale { stroke: #ffb04a; animation: enf-gluehen 1.1s ease-in-out infinite alternate; }
@keyframes enf-gluehen { to { stroke: #ff5a2a; } }
/* --- Plaketten ---------------------------------------------------------- */
.enf .enf-plakette { animation: enf-einblenden .5s both; animation-delay: calc(var(--i) * 80ms); }
.enf .enf-plakette circle,
.enf .enf-plakette rect { fill: #1c2024; stroke: var(--pfarbe); stroke-width: 2; }
.enf .enf-plakette-glyph { font-family: bootstrap-icons; font-size: 16px; fill: var(--pfarbe); text-anchor: middle; dominant-baseline: central; }
.enf .enf-plakette-text { font-size: 14px; font-weight: 600; fill: var(--ton-schrift, #e8ede9); dominant-baseline: central; }
/* --- Bewässerung --------------------------------------------------------- */
.enf .enf-zone-glyph { font-family: bootstrap-icons; font-size: 17px; fill: currentColor; color: #6c757d; text-anchor: middle; dominant-baseline: central; transition: color .4s; }
.enf .enf-zone-name { font-size: 15px; fill: var(--ton-schrift-2, #9aa6ad); dominant-baseline: central; }
.enf .enf-zone-status { font-size: 15px; font-weight: 600; fill: var(--ton-schrift, #e8ede9); text-anchor: end; dominant-baseline: central; font-variant-numeric: tabular-nums; }
/* --- Flüsse -------------------------------------------------------------- */
.enf .enf-bett {
fill: none; stroke: #252b31; stroke-linecap: round;
stroke-width: calc(var(--breite, 0) * 1px + 5px);
stroke-dasharray: 1 1; stroke-dashoffset: 1;
transition: stroke-width .8s;
animation: enf-zeichnen .9s cubic-bezier(.5, 0, .2, 1) forwards; animation-delay: var(--verz);
}
@keyframes enf-zeichnen { to { stroke-dashoffset: 0; } }
.enf .enf-strom,
.enf .enf-strom-hof {
fill: none; stroke: var(--farbe); stroke-linecap: round;
stroke-dasharray: 0 20;
/* Stufen statt stetig: stroke-dashoffset laeuft nicht auf dem Compositor,
jede Stufe ist ein Neuzeichnen. 20 je Abschnitt sehen fluessig aus. */
animation: enf-fliessen var(--dauer, 1.5s) steps(20) infinite;
transition: stroke-width .8s, opacity .6s;
}
.enf .enf-strom { stroke-width: calc(var(--breite, 0) * 1px); }
.enf .enf-strom-hof { stroke-width: calc(var(--breite, 0) * 1.9px); opacity: .14; }
.enf .enf-rueck .enf-strom,
.enf .enf-rueck .enf-strom-hof { animation-direction: reverse; }
.enf .enf-still .enf-strom,
.enf .enf-still .enf-strom-hof { opacity: 0; animation: none; }
@keyframes enf-fliessen { to { stroke-dashoffset: -20; } }
/* --- Auftritt ------------------------------------------------------------ */
.enf .enf-koerper { animation: enf-erscheinen .7s cubic-bezier(.2, .9, .3, 1.25) backwards; animation-delay: var(--verz); }
.enf .enf-name { animation: enf-einblenden .7s backwards; animation-delay: calc(var(--verz) + 150ms); }
@keyframes enf-erscheinen { from { opacity: 0; transform: scale(.55); } }
@keyframes enf-einblenden { from { opacity: 0; } }
.enf.enf-pause,
.enf.enf-pause * { animation-play-state: paused !important; }
/* --- Bearbeiten (Einstellungen) ------------------------------------------ */
.enf.enf-bearbeiten .enf-koerper,
.enf.enf-bearbeiten .enf-name,
.enf.enf-bearbeiten .enf-bett { animation: none; stroke-dashoffset: 0; }
.enf.enf-bearbeiten .enf-ziehbar { cursor: grab; touch-action: none; }
.enf.enf-bearbeiten .enf-zieht { cursor: grabbing; }
.enf.enf-bearbeiten .enf-zieht .enf-hof { stroke: var(--farbe); stroke-width: 3; }
@media (prefers-reduced-motion: reduce) {
.enf, .enf * { animation: none !important; transition: none !important; }
.enf .enf-bett { stroke-dashoffset: 0; }
}
/* --- Einstellungen: Reiter "Übersicht" ------------------------------------ */
.enf-vorschau { background: var(--ton-grund, #191d20); border-radius: var(--ton-radius, .875rem); padding: .5rem; position: sticky; top: 4.5rem; }
.enf-e-zeile {
display: grid; grid-template-columns: auto minmax(0, 1fr) auto 1.2rem; gap: .35rem .7rem; align-items: center;
padding: .45rem 0; border-bottom: 1px solid var(--ton-linie, rgba(138, 148, 158, .2));
}
.enf-e-zeile.enf-e-aus .enf-e-name { opacity: .5; }
.enf-e-name { display: flex; align-items: center; gap: .55rem; margin: 0; }
.enf-e-punkt { flex: 0 0 auto; width: .7rem; height: .7rem; border-radius: 50%; background: var(--farbe); box-shadow: 0 0 0 3px color-mix(in srgb, var(--farbe) 22%, transparent); }
.enf-e-eltern { white-space: nowrap; }
.enf-e-flaechen { grid-column: 2 / -1; padding: .2rem 0 .3rem; }
.enf-e-flaeche { display: grid; grid-template-columns: 2.2rem minmax(0, 1fr) 4.2rem 4.2rem 1.2rem; gap: .35rem; align-items: center; margin-bottom: .3rem; }
.enf-e-flaeche .form-control.form-control-color { width: 2.2rem; height: 1.9rem; padding: .15rem; }
/* --- Karte: Umschalter Übersicht / Anlage -------------------------------- */
.enf-umschalter { display: inline-flex; gap: 2px; padding: 3px; border-radius: var(--ton-radius-klein, .5rem); background: var(--ton-grund, #191d20); }
.enf-umschalter .btn.btn-sm { border: 0; color: var(--ton-schrift-2, #9aa6ad); font-weight: 600; padding: .2rem .8rem; }
.enf-umschalter .btn.btn-sm.active { background: var(--ton-flaeche-2, #2b3237); color: var(--ton-schrift, #e8ede9); }
.enf-ansicht[hidden] { display: none !important; }
.enf-ansicht.enf-rein { animation: enf-ansicht-rein .35s cubic-bezier(.2, .7, .2, 1); }
@keyframes enf-ansicht-rein { from { opacity: 0; transform: scale(.98); } }