Katalog und Renderer, die Elternbeziehung als Struktur, und wie aus Watt eine Zahl, ein gestauchter Ring, ein Fluss mit Punktgroesse und Tempo, ein Fuellstand und Plaketten werden. Dazu quer/hochkant, der Sekundentakt mit seinen drei Sparmassnahmen und der Bearbeitungsmodus. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
286 lines
11 KiB
Markdown
286 lines
11 KiB
Markdown
# Die Solar-Übersicht (Energiefluss)
|
||
|
||
Die Karte „Realtime“ auf der Solar-Seite: Wo kommt der Strom her, wo geht er
|
||
hin, wie viel ist es gerade. Sie ist kein gezeichnetes Bild, sondern ein
|
||
**Katalog plus ein Renderer** — und genau diese Trennung macht sie
|
||
erweiterbar.
|
||
|
||

|
||
|
||
| Datei | Aufgabe |
|
||
|---|---|
|
||
| `js/solar/energieflussKatalog.js` | **Was** es gibt: jeder Kreis genau einmal — Name, Symbol, Farbe, Elternkreis, Lage, Ringskala, Klickziel und `lesen()` |
|
||
| `js/solar/energiefluss.js` | **Wie** gezeichnet wird: Kreise, Ringe, Flüsse, Symbole, Plaketten, Hochkant, Bearbeitungsmodus. Kennt keinen Kreis beim Namen |
|
||
| `restricted/energiefluss.php` | speichert **Abweichungen** je Haus (`homeMesh.energiefluss`) |
|
||
| `js/solar/solarMQTT.js` | hängt beides an den MQTT-Strom und an die Reiter |
|
||
|
||
---
|
||
|
||
## 1. Der Aufbau: Kreise und ihre Eltern
|
||
|
||
Jeder Kreis nennt seinen **Elternkreis**; daraus entsteht der Fluss zwischen
|
||
beiden. Es gibt keine getrennte Liste von Verbindungen — die Struktur ist die
|
||
Elternbeziehung.
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
PV["Sonne<br/><small>pv</small>"] --> WR
|
||
NETZ["Netz<br/><small>netz</small>"] --> WR
|
||
AKKU["Batterie<br/><small>akku</small>"] --> WR(("Autarkie<br/><small>wr · Pflicht</small>"))
|
||
WR --> VERB(("Verbrauch<br/><small>verbrauch · Pflicht</small>"))
|
||
VERB --> OG["Obergeschoss"] --> WBOG["Wallbox OG"]
|
||
VERB --> EG["Erdgeschoss"] --> WBEG["Wallbox EG"]
|
||
VERB --> UG["Untergeschoss"]
|
||
VERB --> HEIZ["Heizstab"]
|
||
BEW["Bewässerung<br/><small>frei stehend</small>"]
|
||
```
|
||
|
||
Ein Eintrag im Katalog sieht so aus:
|
||
|
||
```js
|
||
{
|
||
id: "heizstab", name: "Heizstab", symbol: "speicher", farbe: "#c2614e",
|
||
eltern: "verbrauch", // woher der Fluss kommt
|
||
x: 1120, y: 380, // Lage in der Queransicht
|
||
hochX: 670, hochY: 600, // optional: eigene Lage hochkant
|
||
skala: 6000, // W, bei denen der Ring voll ist
|
||
klick: { modal: "heater" }, // Ziel beim Antippen
|
||
lesen(m, opt) { // aus MQTT-Werten wird ein Zustand
|
||
const w = m.zahl("solarManager/pHeat");
|
||
return { leistung: w, fluss: w, ruhe: w < 20, neben: "…", zeichen: [...],
|
||
zustand: { an: w >= 20, temps: [...] } };
|
||
},
|
||
}
|
||
```
|
||
|
||
Was `lesen()` zurückgeben darf:
|
||
|
||
| Feld | Wirkung |
|
||
|---|---|
|
||
| `leistung` | die große Zahl und der Ring |
|
||
| `wert` | Text **statt** der Leistung (z. B. „93 %“) |
|
||
| `neben` | die kleine Zeile darunter |
|
||
| `segmente` | `[{w, farbe, knoten?}]` statt eines einfarbigen Rings |
|
||
| `fluss` | Watt vom Elternkreis hierher; negativ = Gegenrichtung |
|
||
| `ruhe` | Kreis blass zeichnen — es ist nichts los |
|
||
| `zeichen` | Plaketten am Rand: `[{art, text?, titel?}]` |
|
||
| `zustand` | was das Symbol braucht: `{an, stand, laden, temps}` |
|
||
|
||
> **Ein neuer Kreis ist ein neuer Eintrag hier — sonst nichts.** Kein
|
||
> Markup, keine CSS-Regel, keine Zeile im Renderer. Nur ein neues *Symbol*
|
||
> braucht einen Eintrag in `SYMBOLE` (`energiefluss.js`).
|
||
|
||
### Abweichungen je Haus
|
||
|
||
Der Katalog ist der Normalfall, `homeMesh.energiefluss` hält nur, was davon
|
||
abweicht: Kreis aus, anderer Name, andere Lage, andere Optionen. Eingestellt
|
||
wird das unter **Einstellungen → Übersicht**
|
||
(→ [einstellungen.md](einstellungen.md#3-übersicht-energiefluss)).
|
||
|
||
Ist ein Kreis **aus**, rückt sein Kind automatisch an den nächsten
|
||
eingeschalteten Vorfahren: `elternWirksam` sucht die Kette hinauf. Und ein
|
||
Ringsegment, das zu einem ausgeschalteten Kreis gehört, fällt in das letzte
|
||
verbleibende Segment — die Summe bleibt also richtig, auch wenn jemand die
|
||
Wallbox ausblendet.
|
||
|
||
---
|
||
|
||
## 2. Die Leistungsvisualisierung
|
||
|
||
Drei Dinge zeigen dieselbe Leistung auf drei Arten. Das ist Absicht: Die Zahl
|
||
ist genau, der Ring vergleicht, der Fluss zeigt die Richtung.
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
W["Leistung in Watt<br/><small>aus lesen()</small>"] --> Z["Zahl im Kreis<br/><small>W bis 999, danach kW</small>"]
|
||
W --> R["Ring<br/><small>Anteil an skala, gestaucht</small>"]
|
||
W --> F["Fluss zum Elternkreis<br/><small>Punktgröße + Tempo</small>"]
|
||
W --> S["Symbolzustand<br/><small>an/aus, Füllstand, Temperaturen</small>"]
|
||
```
|
||
|
||
### 2.1 Der Ring — gestauchte Skala
|
||
|
||
Linear wäre der Ring unbrauchbar: Bei `skala = 15000` wären 300 W ein Strich
|
||
von zwei Grad, und genau in diesem Bereich steht die Anlage die meiste Zeit.
|
||
|
||
```
|
||
Anteil = log10(1 + 9 · w/skala)
|
||
|
||
w/skala 0 % 10 % 25 % 50 % 100 %
|
||
Ring 0 % 28 % 47 % 70 % 100 %
|
||
```
|
||
|
||
Der volle Ausschlag bleibt exakt bei `skala`, aber das untere Zehntel bekommt
|
||
knapp ein Drittel des Rings.
|
||
|
||
**Mehrere Segmente** (etwa Verbrauch = Etagen + Heizstab + Rest) werden über
|
||
die **Summe** gerechnet und nicht je Segment — die gestauchte Skala ist nicht
|
||
additiv, sonst reichten drei Viertel-Segmente weiter als ein volles:
|
||
|
||
```
|
||
Segment i = ringAnteil(Σ bis i) − ringAnteil(Σ bis i−1)
|
||
```
|
||
|
||
Zwischen zwei Segmenten bleibt eine Lücke von 0,6 %; Segmente unter 0,4 %
|
||
verschwinden ganz, sonst stünde dort ein Punkt statt eines Bogens.
|
||
|
||
### 2.2 Der Fluss — Punkte statt Linie
|
||
|
||
Ein Fluss ist ein Bogen vom Eltern- zum Kindkreis (`flussPfad()`): senkrecht
|
||
los, dann waagerecht hinein — hochkant umgekehrt. Gezeichnet wird er als
|
||
gestrichelte Linie mit runden Enden, also als Punktreihe, die über
|
||
`stroke-dashoffset` wandert.
|
||
|
||
| Größe | Formel | Wirkung |
|
||
|---|---|---|
|
||
| Punktgröße | `2,4 + 11 · (1 − e^(−W/5000))` | 2,4 px bei ein paar hundert Watt, ~13 px bei Volllast |
|
||
| Tempo | `1 / (0,7 + 1,9 · e^(−W/3000))` | 2,6 s je Abschnitt bei wenig, 0,7 s bei viel |
|
||
| Richtung | Vorzeichen von `fluss` | `enf-rueck` dreht die Animation um |
|
||
| Stillstand | unter 15 W | Punkte ausgeblendet |
|
||
|
||
Zwei Feinheiten, die teuer erkauft waren:
|
||
|
||
* **Das Tempo wird über `Animation.updatePlaybackRate()` gesetzt**, nicht über
|
||
eine neue `animation-duration`. Eine geänderte Dauer rechnet die Position
|
||
neu — die Punkte sprängen bei jeder Wertänderung.
|
||
* **Keine Bahn unter den Punkten.** Die dunkle Linie gibt es nur im
|
||
Bearbeitungsmodus, wo sie zeigt, was mit was verbunden ist. In der Ansicht
|
||
erzählen die Punkte den Weg; wo nichts fließt, steht nichts.
|
||
|
||
### 2.3 Füllstände
|
||
|
||
Batterie und Wallbox haben statt eines bloßen Rings eine **Welle im Kreis**,
|
||
die mit dem Ladestand steigt (`zustand.stand`, 0…1). Die Fläche liegt bei
|
||
22 % Deckung, beim Laden 32 %, und die Wasserlinie ist zusätzlich als Kante
|
||
gezeichnet — sonst muss man den Stand suchen. Meldet ein Kreis keinen Stand,
|
||
bleibt die Füllung ganz weg: Eine Wallbox ohne Auto zeigte sonst eine Linie
|
||
am unteren Rand.
|
||
|
||
### 2.4 Plaketten und Symbolzustände
|
||
|
||
Kleine runde Zeichen am Kreisrand, aus `lesen().zeichen`:
|
||
|
||
| `art` | Zeichen | Bedeutung |
|
||
|---|---|---|
|
||
| `eco` | Blatt | Eco-Modus (Wallbox, Heizstab) |
|
||
| `uhr` | Uhr | „Nächste Fahrt“ |
|
||
| `manuell` | Regler | fester Modus |
|
||
| `stecker`, `schloss` | Stecker, Schloss | Fahrzeug angesteckt / verriegelt |
|
||
| `heizung` | Flamme | Heizung im Geschoss an |
|
||
| `tank` | Zapfsäule | Tankfüllung, mit Text |
|
||
| `warnung` | Dreieck | Störung |
|
||
|
||
Die Glyphen sind Bootstrap-Icons und stehen als `\uF…`-Escapes im Quelltext —
|
||
die Zeichen selbst liegen im privaten Unicode-Bereich, sind im Editor
|
||
unsichtbar und werden beim Kopieren leicht verfälscht.
|
||
|
||
`zustand` steuert dagegen das Symbol selbst: `an` schaltet die
|
||
Zustandsklasse (leuchtender Kern), `laden` die Ladeanimation, `temps` färbt
|
||
die Schichten des Pufferspeichers zwischen kalt und heiß.
|
||
|
||
### 2.5 Die Bewässerung
|
||
|
||
Der einzige Kreis ohne Leistung: Statt Zahl und Ring stehen darin drei
|
||
Zeilen, je Zone ein Symbol, der Name und der Zustand. Gezeichnet werden sie
|
||
aus `katalog.zonen`, gefüllt von `bewaesserungAnzeige.js`, das die Ids
|
||
kennt, die der Renderer vergibt.
|
||
|
||
---
|
||
|
||
## 3. Zwei Ansichten aus einer Lage
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
subgraph Q["quer (≥ 720 px)"]
|
||
direction LR
|
||
Q1["Quellen links"] --> Q2["Mitte"] --> Q3["Verbraucher rechts"]
|
||
end
|
||
subgraph H["hochkant (< 720 px)"]
|
||
direction TB
|
||
H1["Quellen oben"] --> H2["Mitte"] --> H3["Verbraucher unten"]
|
||
end
|
||
```
|
||
|
||
Hochkant werden schlicht **x und y vertauscht**. So braucht es keine zweite
|
||
Lage je Kreis, und eine eigene Lage aus den Einstellungen gilt in beiden
|
||
Ansichten. Wo das Vertauschen unglücklich steht, nennt der Katalog mit
|
||
`hochX`/`hochY` eine eigene Lage — so stehen UG und Heizstab hochkant
|
||
getauscht, damit die drei Geschosse untereinander liegen.
|
||
|
||
Umgeschaltet wird über einen `ResizeObserver` am Behälter, und nur dann, wenn
|
||
die Ansicht wirklich wechselt. Das Kiosk-Display (`anzeige.js`) setzt
|
||
`hochkantUnter: 0` und bleibt damit immer quer.
|
||
|
||
Der `viewBox` wird nach dem Zeichnen aus den äußersten Kreisen gerechnet
|
||
(`rahmenSetzen()`), inklusive Platz für den Namen unter dem Kreis — ein
|
||
ausgeschalteter Randkreis verkleinert das Bild also automatisch.
|
||
|
||
---
|
||
|
||
## 4. Wie die Werte hereinkommen
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant M as MQTT-Broker
|
||
participant S as solarMQTT.js
|
||
participant E as Energiefluss
|
||
participant K as Katalog
|
||
|
||
M->>S: solarManager/# (viele Nachrichten je Sekunde)
|
||
Note over S: Werte in mqttData ablegen,<br/>höchstens 1× pro Sekunde weiterreichen
|
||
S->>E: aktualisieren(mqttData)
|
||
loop je sichtbarem Kreis
|
||
E->>K: lesen(m, optionen)
|
||
K-->>E: {leistung, fluss, zeichen, zustand, …}
|
||
Note over E: nur schreiben, was sich geändert hat
|
||
end
|
||
```
|
||
|
||
Drei Sparmaßnahmen, die zusammen die Karte flüssig halten:
|
||
|
||
1. **Ein Takt je Sekunde.** Der Broker schickt deutlich öfter.
|
||
2. **Nur geänderte Werte schreiben.** Jeder Schreibzugriff macht Style und
|
||
Paint ungültig; gemessen waren rund 71 % der Zugriffe reine
|
||
Wiederholungen.
|
||
3. **Animationen stehen still, wenn niemand hinsieht** — `IntersectionObserver`
|
||
für „Karte im Bild“ und `visibilitychange` für „Tab im Vordergrund“.
|
||
|
||
Ein Fehler in einem `lesen()` fängt der Renderer je Kreis ab: Er meldet ihn
|
||
auf der Konsole und zeichnet die übrigen weiter. Eine kaputte Zeile im
|
||
Katalog nimmt also nicht die ganze Übersicht mit.
|
||
|
||
---
|
||
|
||
## 5. Antippen und Bearbeiten
|
||
|
||
`klick` im Katalog sagt, was ein Tipp auf einen Kreis tut:
|
||
|
||
| Eintrag | Wirkung |
|
||
|---|---|
|
||
| `{ ansicht: "anlage" }` | schaltet auf die Anlagen-Ansicht (`anlage.js`) |
|
||
| `{ ansicht: "speicher" }` | schaltet auf die Speicher-Ansicht (`speicher.js`) |
|
||
| `{ modal: "heater" }` … | öffnet das passende Bedienfenster (`bedienfenster.js`, `ladefenster.js`) |
|
||
|
||
Klickbare Kreise bekommen ein Pfeil-Abzeichen, `tabindex`, `role="button"`
|
||
und reagieren auf Enter und Leertaste.
|
||
|
||
Im **Bearbeitungsmodus** (`bearbeiten: true`, nur in den Einstellungen) sind
|
||
die Kreise ziehbar, die Verbindungslinien sichtbar und die Lage wird beim
|
||
Loslassen auf zehn Einheiten gerundet an `onVerschoben(id, x, y)` gemeldet.
|
||
Die Vorschau dort läuft mit festen Beispielwerten, damit man auch nachts
|
||
sieht, was man einstellt.
|
||
|
||
---
|
||
|
||
## 6. Wo fange ich an, wenn ich …
|
||
|
||
| Vorhaben | Ort |
|
||
|---|---|
|
||
| … einen Kreis hinzufügen | ein Eintrag in `ENERGIEFLUSS_KATALOG.knoten` — mehr nicht |
|
||
| … ein neues Symbol | `SYMBOLE` in `energiefluss.js` (24er Raster, Mitte 0/0, `currentColor`) |
|
||
| … eine neue Plakette | `ZEICHEN` in `energiefluss.js`, Glyph als `\uF…`-Escape |
|
||
| … die Ringspreizung ändern | `ringAnteil()` — der Faktor 9 bestimmt die Stauchung |
|
||
| … Punktgröße oder Tempo | `flussSetzen()` in `energiefluss.js` |
|
||
| … die Lage hochkant korrigieren | `hochX`/`hochY` im Katalog (gilt nur dort) |
|
||
| … einen Kreis ausblenden oder verschieben | Einstellungen → Übersicht, keine Codeänderung |
|