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
+33 -9
View File
@@ -183,13 +183,13 @@ genau die Topics, die auf den Kacheln stehen (`tileTopics()` in `rooms.php`).
| Seite (`?action=`) | Vorlage | Seitenskripte | holt per fetch | hört auf MQTT | | Seite (`?action=`) | Vorlage | Seitenskripte | holt per fetch | hört auf MQTT |
|---|---|---|---|---| |---|---|---|---|---|
| `solar` (Vorgabe) | `solar.php` | `jahresstatistik.js`, `solarMQTT.js` | `getProdData`, `getConsData`, `getForecastData`, `getSunrise`, `getStats`, `carEG`, `carOG`, `heater`, `watering` | `solarManager/#`, `weatherStation/#`, `wattpilot/#`, `go-eCharger/#`, `Gartenwasser/#` | | `solar` (Vorgabe) | `solar.php` | `jahresstatistik.js`, `energieflussKatalog.js`, `energiefluss.js`, `solarMQTT.js` | `getProdData`, `getConsData`, `getForecastData`, `getSunrise`, `getStats`, `carEG`, `carOG`, `heater`, `watering` | `solarManager/#`, `weatherStation/#`, `wattpilot/#`, `go-eCharger/#`, `Gartenwasser/#` |
| `home` | `home.php` | `autoActionFuncs.js`, `zeitleiste.js`, `homeMQTT.js` | `room.php`, `AutoAction.php` | die Topics der Kacheln, `Raumtemp/#` | | `home` | `home.php` | `autoActionFuncs.js`, `zeitleiste.js`, `homeMQTT.js` | `room.php`, `AutoAction.php` | die Topics der Kacheln, `Raumtemp/#` |
| `heat` | `heat.php` | `heatMQTT.js` | `getHeaterData`, `getWaterData`, `getSunrise` | `solarManager/#`, `weatherStation/#`, Wallbox-Topics | | `heat` | `heat.php` | `heatMQTT.js` | `getHeaterData`, `getWaterData`, `getSunrise` | `solarManager/#`, `weatherStation/#`, Wallbox-Topics |
| `history` | `history.php` | `jahresstatistik.js`, `historyMQTT.js` | `energyHistory` (6 ×), `getStats` | — | | `history` | `history.php` | `jahresstatistik.js`, `historyMQTT.js` | `energyHistory` (6 ×), `getStats` | — |
| `skoda` | `skoda.php` | `skodaMQTT.js` | `skoda.php?was=…`, `skodaCmd.php`, `tile.php` | `solarManager/#` | | `skoda` | `skoda.php` | `skodaMQTT.js` | `skoda.php?was=…`, `skodaCmd.php`, `tile.php` | `solarManager/#` |
| `weather` | `weather.php` | `weatherMQTT.js` | nichts — das Meteogramm ist ein fremdes Dokument im `<iframe>` (`js/meteogram.js`) | `weatherStation/#` | | `weather` | `weather.php` | `weatherMQTT.js` | nichts — das Meteogramm ist ein fremdes Dokument im `<iframe>` (`js/meteogram.js`) | `weatherStation/#` |
| `settings` | `settings.php` | `settings.js`, `grundriss.js` | `settings.php?action=…` | — | | `settings` | `settings.php` | `settings.js`, `grundriss.js`, `energieflussKatalog.js`, `energiefluss.js`, `energieflussEinstellungen.js` | `settings.php?action=…` | — |
| `logs` | `logs.php` | `logs.js` | `logs.php?action=…` | — | | `logs` | `logs.php` | `logs.js` | `logs.php?action=…` | — |
| `einfuehrung` | `einfuehrung.php` | `einfuehrung.js` | nichts — Folien und Bilder stehen in der Seite | — | | `einfuehrung` | `einfuehrung.php` | `einfuehrung.js` | nichts — Folien und Bilder stehen in der Seite | — |
@@ -247,6 +247,8 @@ flowchart TB
SETA --> COSTS & KACH & AUTOM SETA --> COSTS & KACH & AUTOM
STATS --> COSTS STATS --> COSTS
SETA --> GRM["grundriss.php"] SETA --> GRM["grundriss.php"]
SETA --> ENF["energiefluss.php"]
ENF --> HOMEMESH
KACH --> ROOMS KACH --> ROOMS
GRM --> ROOMS GRM --> ROOMS
LOGA --> LOGD LOGA --> LOGD
@@ -300,12 +302,31 @@ Kilowatt melden — die Wallboxen), runden (`stellen`), Einheit anhängen. Klick
öffnet das Raum-Modal über `ajax/room.php?room=<Raumnummer>`; Heizsymbol, öffnet das Raum-Modal über `ajax/room.php?room=<Raumnummer>`; Heizsymbol,
Regler und Ist-Werte lesen aus dem Thermostat-Zweig des Raums (`mqttZweig()`). Regler und Ist-Werte lesen aus dem Thermostat-Zweig des Raums (`mqttZweig()`).
**`solarMQTT.js`** — die große Anlagenübersicht. `solarSVG` zeichnet den **`solarMQTT.js`** — die Solar-Seite. Startet die Energiefluss-Übersicht
Energiefluss (Breite der Pfeile aus der Leistung), darunter hängen die drei (`energieflussStarten()`), füllt den PV-Lageplan hinter der Sonne
Diagramme Prognose, Verbrauch und Erzeugung über `getData()`. Änderungen am SVG laufen über kleine Setter (`assets/img/pvplan.svg`, Ids `det_pvNP`, `invList`) und die Bewässerungszonen,
(`setText`, `setAttr`, `setFlowWidth`) und werden pro Bildrahmen gesammelt darunter hängen die Diagramme Prognose, Verbrauch und Erzeugung über
(`solarSvgUpdatePending`), damit ein Schwall MQTT-Nachrichten nicht hundertmal `getData()`. Aktualisiert wird höchstens einmal je Sekunde
Layout auslöst. (`solarSvgUpdatePending`); die Setter (`setText`, `setAttr`) schreiben nur,
was sich geändert hat.
**Energiefluss-Übersicht** — drei Dateien mit klarer Arbeitsteilung:
| Datei | enthält |
|---|---|
| `energieflussKatalog.js` | **jeden Kreis genau einmal**: Name, Symbol, Farbe, Elternkreis, Lage, Ringskala, Klickziel und `lesen(m, opt)`, das aus den MQTT-Werten Leistung, Zeile darunter, Ringsegmente, Fluss, Plaketten und Symbolzustand macht. Ein neuer Kreis ist ein neuer Eintrag hier — sonst nichts. |
| `energiefluss.js` | den Renderer (`new Energiefluss(behälter, opt)`): Symbole (24er-Raster, animierbare Teile), Kreise mit Ring und Füllstandswelle, Flüsse als Bögen mit laufenden Punkten, Plaketten, Hochkant-Ansicht (x/y vertauscht unter 720 px Breite), Bearbeitungsmodus mit Ziehen. Kennt keinen Kreis beim Namen. |
| `energieflussEinstellungen.js` | Reiter „Übersicht“ in den Einstellungen: Vorschau mit Beispielwerten, Kreise an/aus, Name, Lage per Ziehen, PV-Flächen (welche Einträge von `P_PVn` zu welcher Fläche gehören). |
Was ein Haus vom Katalog abweicht, steht in `homeMesh.energiefluss`
(`homeMesh_energiefluss.sql`, gelesen/geschrieben in `restricted/energiefluss.php`) —
nur echte Abweichungen, eine leere Tabelle ist der Katalog. Ist ein Kreis aus,
hängen seine Kinder am nächsten eingeschalteten darüber, und sein Anteil im
Ring des Elternkreises fällt in das letzte Segment. Die Animationen stehen
still, solange die Karte nicht zu sehen ist, und entfallen bei
`prefers-reduced-motion`. Stile: Abschnitt „Energiefluss-Übersicht“ in
`css/solar.css`. Bis September 2026 stand hier `assets/img/realtime.svg` mit
festen Kreisen.
**`skodaMQTT.js`** — Fahrzeugseite. Holt alles über `ajax/skoda.php?was=…` **`skodaMQTT.js`** — Fahrzeugseite. Holt alles über `ajax/skoda.php?was=…`
(live, ladungen, kurve, gesundheit, fahrten, strecke), zeichnet das Fahrzeug (live, ladungen, kurve, gesundheit, fahrten, strecke), zeichnet das Fahrzeug
@@ -490,6 +511,7 @@ Haus hätte man den Quelltext umschreiben müssen.
|---|---| |---|---|
| `floors` | `code` (fest: URL, SVG-Ids, `automations.floor`), `label`, Reihenfolge, Grundrissbild, Standard-Etage | | `floors` | `code` (fest: URL, SVG-Ids, `automations.floor`), `label`, Reihenfolge, Grundrissbild, Standard-Etage |
| `rooms` | feste Nummer `id`, Etage, Name, `kuerzel` (SVG-Ids), Kachel `x`/`y` (leer = keine Kachel), `thermostat` (MQTT-Zweig), `werte` (JSON, leer = Vorgabe) | | `rooms` | feste Nummer `id`, Etage, Name, `kuerzel` (SVG-Ids), Kachel `x`/`y` (leer = keine Kachel), `thermostat` (MQTT-Zweig), `werte` (JSON, leer = Vorgabe) |
| `energiefluss` | Abweichungen der Solar-Übersicht vom Katalog: `aktiv`, `name`, `x`/`y`, `optionen` (JSON) — eigene Datei `homeMesh_energiefluss.sql`, keine Zeile = Katalog |
Auf die feste Raumnummer zeigen `actors.room_id` und das Raum-Modal Auf die feste Raumnummer zeigen `actors.room_id` und das Raum-Modal
(`room.php?room=12`). Ein umbenannter Raum behält damit Geräte, Regler und (`room.php?room=12`). Ein umbenannter Raum behält damit Geräte, Regler und
@@ -816,7 +838,9 @@ ausgeliefert.
|---|---| |---|---|
| Ein Raum oder eine Etage kommt dazu, heißt anders, bekommt ein neues Grundrissbild | Einstellungen → Grundriss (keine Codeänderung) — Kachel, Menü und MQTT-Abo ergeben sich daraus | | Ein Raum oder eine Etage kommt dazu, heißt anders, bekommt ein neues Grundrissbild | Einstellungen → Grundriss (keine Codeänderung) — Kachel, Menü und MQTT-Abo ergeben sich daraus |
| Andere Werte auf einer Kachel | Einstellungen → Home-Kacheln (keine Codeänderung). Vorgabe ohne eigene Werte: `kachelVorgabe()` in `rooms.php` | | Andere Werte auf einer Kachel | Einstellungen → Home-Kacheln (keine Codeänderung). Vorgabe ohne eigene Werte: `kachelVorgabe()` in `rooms.php` |
| Ein anderes Haus einrichten | `homeMesh_DB-layout.sql`, `homeMesh_automations.sql`, `homeMesh_grundriss.sql` in dieser Reihenfolge; dann Einstellungen → Grundriss | | Ein anderes Haus einrichten | `homeMesh_DB-layout.sql`, `homeMesh_automations.sql`, `homeMesh_grundriss.sql`, `homeMesh_energiefluss.sql` in dieser Reihenfolge; dann Einstellungen → Grundriss und → Übersicht |
| Kreis der Energiefluss-Übersicht aus, umbenennen, verschieben; PV-Flächen | Einstellungen → Übersicht (keine Codeänderung) |
| Neuer Kreis in der Energiefluss-Übersicht (Gerät, Etage, Quelle) | ein Eintrag in `js/solar/energieflussKatalog.js`; neues Symbol dazu in `SYMBOLE` (`energiefluss.js`) |
| Neues Symbol auf einer Kachel | Name eines Bootstrap-Icons ins Feld tippen. Vorschlagsliste: `kachelSymbole()` in `kacheln.php` | | Neues Symbol auf einer Kachel | Name eines Bootstrap-Icons ins Feld tippen. Vorschlagsliste: `kachelSymbole()` in `kacheln.php` |
| Neue Kennzahl in der Jahresstatistik | `KENNZAHLEN` **und** `kennzahlenAus()` in `ajax/getStats.php` — sonst nichts | | Neue Kennzahl in der Jahresstatistik | `KENNZAHLEN` **und** `kennzahlenAus()` in `ajax/getStats.php` — sonst nichts |
| Neues Diagramm auf einer Seite | Karte in der Vorlage, `<canvas>`, dazu ein Endpunkt nach dem Muster von `getProdData.php`, geladen mit `getData()` | | Neues Diagramm auf einer Seite | Karte in der Vorlage, `<canvas>`, dazu ein Endpunkt nach dem Muster von `getProdData.php`, geladen mit `getData()` |
+18
View File
@@ -24,6 +24,9 @@
* POST ?action=raum-loeschen {"nr": 12} * POST ?action=raum-loeschen {"nr": 12}
* Die Grundriss-Aktionen antworten mit dem ganzen neuen Stand. * Die Grundriss-Aktionen antworten mit dem ganzen neuen Stand.
* *
* GET ?action=energiefluss Abweichungen der Übersicht vom Katalog
* POST ?action=energiefluss-save {"knoten": [{id, aktiv, name, x, y, optionen}, ...]}
*
* Ein reiner Verteiler: geprueft und geschrieben wird in * Ein reiner Verteiler: geprueft und geschrieben wird in
* restricted/costs.php, kacheln.php bzw. grundriss.php, damit Maske und * restricted/costs.php, kacheln.php bzw. grundriss.php, damit Maske und
* Endpunkt dieselbe Wahrheit ueber einen Zeitraum oder eine Kachel haben. * Endpunkt dieselbe Wahrheit ueber einen Zeitraum oder eine Kachel haben.
@@ -35,6 +38,7 @@ require_once("../restricted/kacheln.php");
require_once("../restricted/automations.php"); require_once("../restricted/automations.php");
require_once("../restricted/skodaKeys.php"); require_once("../restricted/skodaKeys.php");
require_once("../restricted/grundriss.php"); require_once("../restricted/grundriss.php");
require_once("../restricted/energiefluss.php");
if (!checkLogin()) { if (!checkLogin()) {
http_response_code(403); http_response_code(403);
@@ -140,6 +144,12 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
grundriss(true); grundriss(true);
jsonAntwort(["stand" => grundrissStand()]); jsonAntwort(["stand" => grundrissStand()]);
case "energiefluss-save":
// Zurück kommt der gespeicherte Stand - ohne die Einträge,
// die dem Katalog entsprechen.
energieflussSpeichern($body["knoten"] ?? null);
jsonAntwort(["einstellungen" => (object)energieflussLaden()]);
case "skoda-keys-save": case "skoda-keys-save":
/* /*
* Neue Schluessel eintragen. Zurueck kommt der neue Stand - * Neue Schluessel eintragen. Zurueck kommt der neue Stand -
@@ -188,6 +198,14 @@ if ($action === "grundriss") {
} }
} }
if ($action === "energiefluss") {
try {
jsonAntwort(["einstellungen" => (object)energieflussLaden()]);
} catch (Throwable $e) {
jsonAntwort(["error" => "Übersicht nicht abrufbar: " . $e->getMessage()], 500);
}
}
if ($action === "skoda-keys") { if ($action === "skoda-keys") {
try { try {
jsonAntwort(skodaStand()); jsonAntwort(skodaStand());
+192
View File
@@ -0,0 +1,192 @@
<!-- Lageplan der PV-Platten, bis September 2026 Teil von realtime.svg (Gruppe
pvDetail, geoeffnet ueber die Sonne). Unveraendert herausgeloest, damit die
neue Energiefluss-Uebersicht ihn als eigene Ansicht zeigen kann. Die Werte
schreibt solarMQTT.js ueber die Ids det_pvNP und invList. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="40 0 1260 740" width="100%" class="enf-pvplan">
<style>
.enf-pvplan .infoTextClass { font-size: 18px; font-family: sans-serif; text-anchor: middle; fill: rgb(200, 210, 210); }
.enf-pvplan .tooltipTextClass { font-size: 18px; font-family: sans-serif; text-anchor: start; fill: rgb(200, 210, 210); }
</style>
<defs>
<filter id="shadowMiddle" x="-0.4" y="-0.4" width="180%" height="180%">
<feMorphology in="SourceAlpha" result="offOut" operator="dilate" radius="3" />
<feGaussianBlur result="blurOut" in="offOut" stdDeviation="1" />
<feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
</filter>
<filter id="shadowBig" x="-0.45" y="-0.45" width="190%" height="190%">
<feMorphology in="SourceAlpha" result="offOut" operator="dilate" radius="3" />
<feGaussianBlur result="offOut" in="offOut" stdDeviation="2" />
<feBlend in="SourceGraphic" in2="offOut" mode="normal" />
</filter>
<g id="pvPlate" fill="none" stroke-width="4" stroke="#f7c000">
<rect width="70" height="40" x="0" y="0" rx="5" ry="5" />
</g>
<g id="northIcon" fill="#808090">
<path
d="M42.066 0v20h3.752V6.957L53.881 20h4.053V0h-3.752v13.355L45.996 0zm5.57 26.688l-25.77 69.949c-.823 2.238 1.658 4.248 3.677 2.978L50 84.195l24.455 15.42c2.02 1.273 4.504-.738 3.68-2.978l-25.79-70c-.472-1.096-1.283-1.632-2.384-1.635c-1.1-.003-2.017.856-2.324 1.686zm-.136 14.83V79.86L29.1 91.463z"></path>
</g>
</defs>
<g id="pvDetail">
<use href="#northIcon" transform="translate(1200,10),rotate(10)" />
<!--<polygon points="370 0, 1300 0, 1300 700, 0 700, 0 250, 370 250" fill="rgb(24, 27, 31)" />-->
<polygon points="560,80 720,80 720,300 1160,300 1160,370 560,370" stroke-linejoin="round"
fill="#f7c002" stroke-width="30" stroke="#f7c002" opacity="0.1" />
<use href="#pvPlate" transform="translate(600,80), rotate(90)" />
<use href="#pvPlate" transform="translate(600,150), rotate(90)" />
<use href="#pvPlate" transform="translate(600,220), rotate(90)" />
<use href="#pvPlate" transform="translate(600,290), rotate(90)" />
<use href="#pvPlate" transform="translate(640,80), rotate(90)" />
<use href="#pvPlate" transform="translate(640,150), rotate(90)" />
<use href="#pvPlate" transform="translate(640,220), rotate(90)" />
<use href="#pvPlate" transform="translate(640,290), rotate(90)" />
<use href="#pvPlate" transform="translate(680,80), rotate(90)" />
<use href="#pvPlate" transform="translate(680,150), rotate(90)" />
<use href="#pvPlate" transform="translate(680,220), rotate(90)" />
<use href="#pvPlate" transform="translate(680,290), rotate(90)" />
<use href="#pvPlate" transform="translate(720,80), rotate(90)" />
<use href="#pvPlate" transform="translate(720,150), rotate(90)" />
<use href="#pvPlate" transform="translate(720,220), rotate(90)" />
<use href="#pvPlate" transform="translate(790,290)" />
<use href="#pvPlate" transform="translate(720,330)" />
<use href="#pvPlate" transform="translate(790,330)" />
<use href="#pvPlate" transform="translate(870,290)" />
<use href="#pvPlate" transform="translate(870,330)" />
<use href="#pvPlate" transform="translate(940,330)" />
<use href="#pvPlate" transform="translate(1160,300), rotate(90)" />
<use href="#pvPlate" transform="translate(1120,300), rotate(90)" />
<use href="#pvPlate" transform="translate(1080,300), rotate(90)" />
<text id="det_pv0P" filter="url(#shadowBig)" class="infoTextClass" dx="55" dy="30"
transform="translate(560, 150), scale(1.5,1.5)">-.-- kW</text>
<polygon points="980,60 1160,60 1160,265 980,265" stroke-linejoin="round" fill="#F76D00"
stroke-width="30" stroke="#F76D00" opacity="0.1" />
<use href="#pvPlate" transform="translate(1160,200), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1115,200), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1070,200), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1160,130), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1115,130), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1070,130), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1025,130), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1160,60), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1115,60), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1070,60), rotate(90), scale(1,1.125)" />
<text id="det_pv1P" filter="url(#shadowBig)" class="infoTextClass" dx="55" dy="30"
transform="translate(990, 140), scale(1.5,1.5)">-.-- kW</text>
<use href="#pvPlate" x="200" y="680" />
<text id="det_pv33P" filter="url(#shadowMiddle)" class="infoTextClass" x="200" y="680" dx="36"
dy="26">#19</text>
<use href="#pvPlate" x="380" y="550" />
<text id="det_pv32P" filter="url(#shadowMiddle)" class="infoTextClass" x="380" y="550" dx="36"
dy="26">#19</text>
<use href="#pvPlate" x="380" y="590" />
<text id="det_pv31P" filter="url(#shadowMiddle)" class="infoTextClass" x="380" y="590" dx="36"
dy="26">#18</text>
<use href="#pvPlate" x="380" y="630" />
<text id="det_pv30P" filter="url(#shadowMiddle)" class="infoTextClass" x="380" y="630" dx="36"
dy="26">#17</text>
<use href="#pvPlate" x="310" y="510" />
<text id="det_pv29P" filter="url(#shadowMiddle)" class="infoTextClass" x="310" y="510" dx="36"
dy="26">#16</text>
<use href="#pvPlate" x="310" y="550" />
<text id="det_pv28P" filter="url(#shadowMiddle)" class="infoTextClass" x="310" y="550" dx="36"
dy="26">#15</text>
<use href="#pvPlate" x="310" y="590" />
<text id="det_pv27P" filter="url(#shadowMiddle)" class="infoTextClass" x="310" y="590" dx="36"
dy="26">#14</text>
<use href="#pvPlate" x="310" y="630" />
<text id="det_pv26P" filter="url(#shadowMiddle)" class="infoTextClass" x="310" y="630" dx="36"
dy="26">#13</text>
<use href="#pvPlate" x="240" y="510" />
<text id="det_pv25P" filter="url(#shadowMiddle)" class="infoTextClass" x="240" y="510" dx="36"
dy="26">#12</text>
<use href="#pvPlate" x="240" y="550" />
<text id="det_pv24P" filter="url(#shadowMiddle)" class="infoTextClass" x="240" y="550" dx="36"
dy="26">#11</text>
<use href="#pvPlate" x="240" y="590" />
<text id="det_pv23P" filter="url(#shadowMiddle)" class="infoTextClass" x="240" y="590" dx="36"
dy="26">#10</text>
<use href="#pvPlate" x="240" y="630" />
<text id="det_pv22P" filter="url(#shadowMiddle)" class="infoTextClass" x="240" y="630" dx="36"
dy="26">#9</text>
<use href="#pvPlate" x="170" y="510" />
<text id="det_pv20P" filter="url(#shadowMiddle)" class="infoTextClass" x="170" y="510" dx="36"
dy="26">#8</text>
<use href="#pvPlate" x="170" y="550" />
<text id="det_pv21P" filter="url(#shadowMiddle)" class="infoTextClass" x="170" y="550" dx="36"
dy="26">#7</text>
<use href="#pvPlate" x="170" y="590" />
<text id="det_pv19P" filter="url(#shadowMiddle)" class="infoTextClass" x="170" y="590" dx="36"
dy="26">#6</text>
<use href="#pvPlate" x="170" y="630" />
<text id="det_pv18P" filter="url(#shadowMiddle)" class="infoTextClass" x="170" y="630" dx="36"
dy="26">#5</text>
<use href="#pvPlate" x="100" y="510" />
<text id="det_pv14P" filter="url(#shadowMiddle)" class="infoTextClass" x="100" y="510" dx="36"
dy="26">#1</text>
<use href="#pvPlate" x="100" y="550" />
<text id="det_pv15P" filter="url(#shadowMiddle)" class="infoTextClass" x="100" y="550" dx="36"
dy="26">#2</text>
<use href="#pvPlate" x="100" y="590" />
<text id="det_pv16P" filter="url(#shadowMiddle)" class="infoTextClass" x="100" y="590" dx="36"
dy="26">#3</text>
<use href="#pvPlate" x="100" y="630" />
<text id="det_pv17P" filter="url(#shadowMiddle)" class="infoTextClass" x="100" y="630" dx="36"
dy="26">#4</text>
<use href="#pvPlate" transform="translate(600,500)" />
<text id="det_pv13P" filter="url(#shadowMiddle)" class="infoTextClass" x="600" y="500" dx="36"
dy="26">- W</text>
<use href="#pvPlate" transform="translate(500,500)" />
<text id="det_pv12P" filter="url(#shadowMiddle)" class="infoTextClass" x="500" y="500" dx="36"
dy="26">- W</text>
<use href="#pvPlate" transform="translate(460,120), rotate(-90)" />
<text id="det_pv5P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(460,120), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(460,190), rotate(-90)" />
<text id="det_pv4P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(460,190), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(460,270), rotate(-90)" />
<text id="det_pv9P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(460,270), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(460,340), rotate(-90)" />
<text id="det_pv8P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(460,340), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(460,420), rotate(-90)" />
<text id="det_pv11P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(460,420), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(460,490), rotate(-90)" />
<text id="det_pv10P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(460,490), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(400,120), rotate(-90)" />
<text id="det_pv7P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(400,120), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(400,190), rotate(-90)" />
<text id="det_pv6P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(400,190), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(400,270), rotate(-90)" />
<text id="det_pv3P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(400,270), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(400,340), rotate(-90)" />
<text id="det_pv2P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(400,340), rotate(-90)" dx="36" dy="26">- W</text>
<text id="invList" filter="url(#shadowMiddle)" class="tooltipTextClass"
transform="translate(80,255)"></text>
<rect width="305" height="210" x="70" y="250" rx="5" ry="5" fill="none" stroke-width="4px"
stroke="#f7c000" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

-893
View File
@@ -1,893 +0,0 @@
<style>
/* Fluss-Animation als transform statt stroke-dashoffset.
stroke-dashoffset ist nicht kompositierbar - jede Stufe kostete einen
Neuzeichenvorgang auf dem Hauptthread. Ein transform kann der Compositor
dagegen selbst verschieben.
Die Punkte laufen eine Periode (111 Einheiten) weit, danach steht der
naechste an derselben Stelle - der Zyklus schliesst nahtlos.
--dx/--dy stehen pro Linie im style-Attribut, --w setzt setFlowWidth().
steps(40) = 20 Bewegungen pro Sekunde, wie in der urspruenglichen
Fassung. Das deckelt die Neuzeichnungen fuer den Fall, dass der Browser
das transform doch nicht kompositiert; auf dem Compositor kostet die
Stufung nichts. Fuer volle Fluessigkeit: linear statt steps(40). */
.stream, .stream-rev {
animation: flow 2s steps(40) infinite;
}
.stream-rev {
animation-direction: reverse;
}
@keyframes flow {
to { transform: translate(var(--dx), var(--dy)); }
}
.stream circle, .stream-rev circle {
r: calc(var(--w, 0px) / 2);
fill: gray;
}
.powerTextClass {
font-size: 27px;
font-family: sans-serif;
text-anchor: middle;
fill: rgb(200, 210, 210);
letter-spacing: 0px;
}
.infoTextClass {
font-size: 18px;
font-family: sans-serif;
text-anchor: middle;
fill: rgb(200, 210, 210);
}
.tooltipTextClass {
font-size: 18px;
font-family: sans-serif;
text-anchor: left;
fill: rgb(200, 210, 210);
}
.clickable {
cursor: pointer;
}
</style>
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="-100 -100 1600 950" width="100%" height="100%" id="realtimeSvgAni">
<defs>
<filter
id="glow" filterUnits="userSpaceOnUse"
x="-20%" y="-20%" width="120%" height="120%">
<feGaussianBlur in="SourceGraphic" stdDeviation="7" result="blur5" />
<feGaussianBlur in="SourceGraphic" stdDeviation="10" result="blur10" />
<feGaussianBlur in="SourceGraphic" stdDeviation="14" result="blur20" />
<feGaussianBlur in="SourceGraphic" stdDeviation="17" result="blur30" />
<feGaussianBlur in="SourceGraphic" stdDeviation="20" result="blur50" />
<feMerge result="blur-merged">
<feMergeNode in="blur5" />
<feMergeNode in="blur10" />
<feMergeNode in="blur10" />
<feMergeNode in="blur20" />
</feMerge>
<feMerge>
<feMergeNode in="blur-merged" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<clipPath id="clip_consOGani"><rect x="-30" y="-30" width="404.4" height="60" transform="translate(770.0,380.0) rotate(-64.1790)"/></clipPath>
<clipPath id="clip_consAni"><rect x="-30" y="-30" width="400.0" height="60" transform="translate(430.0,380.0) rotate(0.0000)"/></clipPath>
<clipPath id="clip_consEGani"><rect x="-30" y="-30" width="386.5" height="60" transform="translate(770.0,380.0) rotate(62.6501)"/></clipPath>
<clipPath id="clip_consUGani"><rect x="-30" y="-30" width="396.2" height="60" transform="translate(770.0,380.0) rotate(120.3791)"/></clipPath>
<clipPath id="clip_consHeatAni"><rect x="-30" y="-30" width="410.0" height="60" transform="translate(770.0,380.0) rotate(0.0000)"/></clipPath>
<clipPath id="clip_consEVani"><rect x="-30" y="-30" width="360.0" height="60" transform="translate(920.0,670.0) rotate(0.0000)"/></clipPath>
<clipPath id="clip_consEVOGani"><rect x="-30" y="-30" width="360.0" height="60" transform="translate(920.0,70.0) rotate(0.0000)"/></clipPath>
<clipPath id="clip_consGridani"><rect x="-30" y="-30" width="390.0" height="60" transform="translate(100.0,380.0) rotate(0.0000)"/></clipPath>
<clipPath id="clip_pvani"><rect x="-30" y="-30" width="413.6" height="60" transform="translate(180.0,130.0) rotate(45.0000)"/></clipPath>
<clipPath id="clip_battani"><rect x="-30" y="-30" width="413.6" height="60" transform="translate(180.0,630.0) rotate(-45.0000)"/></clipPath>
</defs>
<g opacity="0">
<animate begin="lastInitAni.end" attributetype="CSS" attributeName="opacity" from="0.0" to="1.0"
dur="0.5s" repeatCount="1" calcMode="spline" keyTimes="0;1" keySplines=".1,0,.27,1"
fill="freeze" />
<animate begin="pvInfo.click" attributetype="CSS" attributeName="opacity" from="1.0" to="0.0"
dur="1s"
repeatCount="1" calcMode="spline" keyTimes="0;1" keySplines=".1,0,.27,1" fill="freeze" />
<animate begin="closeDetails.click" attributetype="CSS" attributeName="opacity" from="0.0"
to="1.0"
dur="1s" repeatCount="1" calcMode="spline" keyTimes="0;1" keySplines=".1,0,.27,1"
fill="freeze" />
<!-- CONS pos: 330, 280
+340, 0 (center +100 in x und y)
Cons OG pos: 330,280
+500 -300 (center +90 in x und y)
-->
<line x1="770" y1="380" x2="920" y2="70" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_consOGani)"><g id="consOGani" class="stream" style="--dx:48.347px;--dy:-99.918px"><circle cx="721.7" cy="479.9"/><circle cx="770.0" cy="380.0"/><circle cx="818.3" cy="280.1"/><circle cx="866.7" cy="180.2"/><circle cx="915.0" cy="80.2"/><circle cx="963.4" cy="-19.7"/></g></g>
<line x1="430" y1="380" x2="770" y2="380" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_consAni)"><g id="consAni" class="stream" style="--dx:111.000px;--dy:0.000px"><circle cx="319.0" cy="380.0"/><circle cx="430.0" cy="380.0"/><circle cx="541.0" cy="380.0"/><circle cx="652.0" cy="380.0"/><circle cx="763.0" cy="380.0"/><circle cx="874.0" cy="380.0"/></g></g>
<line x1="770" y1="380" x2="920" y2="670" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_consEGani)"><g id="consEGani" class="stream" style="--dx:50.996px;--dy:98.592px"><circle cx="719.0" cy="281.4"/><circle cx="770.0" cy="380.0"/><circle cx="821.0" cy="478.6"/><circle cx="872.0" cy="577.2"/><circle cx="923.0" cy="675.8"/></g></g>
<line x1="770" y1="380" x2="600" y2="670" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_consUGani)"><g id="consUGani" class="stream" style="--dx:-56.135px;--dy:95.759px"><circle cx="826.1" cy="284.2"/><circle cx="770.0" cy="380.0"/><circle cx="713.9" cy="475.8"/><circle cx="657.7" cy="571.5"/><circle cx="601.6" cy="667.3"/><circle cx="545.5" cy="763.0"/></g></g>
<line x1="770" y1="380" x2="1120" y2="380" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_consHeatAni)"><g id="consHeatAni" class="stream" style="--dx:111.000px;--dy:0.000px"><circle cx="659.0" cy="380.0"/><circle cx="770.0" cy="380.0"/><circle cx="881.0" cy="380.0"/><circle cx="992.0" cy="380.0"/><circle cx="1103.0" cy="380.0"/><circle cx="1214.0" cy="380.0"/></g></g>
<line x1="920" y1="670" x2="1220" y2="670" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_consEVani)"><g id="consEVani" class="stream" style="--dx:111.000px;--dy:0.000px"><circle cx="809.0" cy="670.0"/><circle cx="920.0" cy="670.0"/><circle cx="1031.0" cy="670.0"/><circle cx="1142.0" cy="670.0"/><circle cx="1253.0" cy="670.0"/></g></g>
<line x1="920" y1="70" x2="1220" y2="70" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_consEVOGani)"><g id="consEVOGani" class="stream" style="--dx:111.000px;--dy:0.000px"><circle cx="809.0" cy="70.0"/><circle cx="920.0" cy="70.0"/><circle cx="1031.0" cy="70.0"/><circle cx="1142.0" cy="70.0"/><circle cx="1253.0" cy="70.0"/></g></g>
<line x1="100" y1="380" x2="430" y2="380" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_consGridani)"><g id="consGridani" class="stream" style="--dx:111.000px;--dy:0.000px"><circle cx="-11.0" cy="380.0"/><circle cx="100.0" cy="380.0"/><circle cx="211.0" cy="380.0"/><circle cx="322.0" cy="380.0"/><circle cx="433.0" cy="380.0"/></g></g>
<line x1="180" y1="130" x2="430" y2="380" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_pvani)"><g id="pvani" class="stream" style="--dx:78.489px;--dy:78.489px"><circle cx="101.5" cy="51.5"/><circle cx="180.0" cy="130.0"/><circle cx="258.5" cy="208.5"/><circle cx="337.0" cy="287.0"/><circle cx="415.5" cy="365.5"/><circle cx="494.0" cy="444.0"/></g></g>
<line x1="180" y1="630" x2="430" y2="380" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_battani)"><g id="battani" class="stream" style="--dx:78.489px;--dy:-78.489px"><circle cx="101.5" cy="708.5"/><circle cx="180.0" cy="630.0"/><circle cx="258.5" cy="551.5"/><circle cx="337.0" cy="473.0"/><circle cx="415.5" cy="394.5"/><circle cx="494.0" cy="316.0"/></g></g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg"
style="position:absolute;top:0;"
viewBox="-100 -100 1600 950" width="100%" height="100%" id="realtimeSvg">
<defs>
<filter id="shadow" x="-0.3" y="-0.3" width="160%" height="160%">
<feMorphology in="SourceAlpha" result="offOut" operator="dilate" radius="0.7" />
<feGaussianBlur result="blurOut" in="offOut" stdDeviation="0.5" />
<feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
</filter>
<filter id="shadowMiddle" x="-0.4" y="-0.4" width="180%" height="180%">
<feMorphology in="SourceAlpha" result="offOut" operator="dilate" radius="3" />
<feGaussianBlur result="blurOut" in="offOut" stdDeviation="1" />
<feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
</filter>
<filter id="shadowBig" x="-0.45" y="-0.45" width="190%" height="190%">
<feMorphology in="SourceAlpha" result="offOut" operator="dilate" radius="3" />
<feGaussianBlur result="offOut" in="offOut" stdDeviation="2" />
<feBlend in="SourceGraphic" in2="offOut" mode="normal" />
</filter>
<g id="circGrid">
<line x2='27.7' y2='76.5' x1='-18.9' y1='61.4' style='stroke-width:4px' />
<line x2='16.5' y2='115.9' x1='-8.1' y1='120.6' style='stroke:#222;stroke-width:2;' />
<line x2='34.5' y2='154.2' x1='15.2' y1='170.1' style='stroke:#222;stroke-width:2;' />
<line x2='68.7' y2='179' x1='59.5' y1='202.3' style='stroke:#222;stroke-width:2;' />
<line x2='110.7' y2='184.3' x1='113.8' y1='209.1' style='stroke:#222;stroke-width:2;' />
<line x2='150' y2='168.8' x1='164.7' y1='189' style='stroke:#222;stroke-width:2;' />
<line x2='176.9' y2='136.2' x1='199.5' y1='146.8' style='stroke:#222;stroke-width:2;' />
<line x2='184.8' y2='94.7' x1='209.8' y1='93.1' style='stroke:#222;stroke-width:2;' />
<line x2='171.8' y2='54.5' x1='192.9' y1='41.1' style='stroke:#222;stroke-width:2;' />
<line x2='140.9' y2='25.5' x1='153' y1='3.6' style='stroke:#222;stroke-width:2;' />
<line x2='100' y2='24' x1='100' y1='-24' style='stroke-width:4px' />
</g>
<g id="circGrid6kW">
<line x2='27.7' y2='76.5' x1='-18.9' y1='61.4' style='stroke-width:4px' />
<line x2='26.4' y2='142.5' x1='4.7' y1='155' style='stroke:#222;stroke-width:2;' />
<line x2='82.3' y2='183.1' x1='77.1' y1='207.6' style='stroke:#555;stroke-width:4px;' />
<line x2='149.9' y2='168.8' x1='164.6' y1='189.0' style='stroke:#222;stroke-width:2;' />
<line x2='184.5' y2='108.9' x1='209.4' y1='111.5' style='stroke:#555;stroke-width:4px;' />
<line x2='163.2' y2='43.1' x1='181.7' y1='26.4' style='stroke:#222;stroke-width:2;' />
<line x2='100' y2='24' x1='100' y1='-24' style='stroke-width:4px' />
</g>
<linearGradient id="gradient1" x1="0" x2="0" y1="0" y2="1">
<stop id="tmpH" offset="0%" stop-color="#2389BA" stop-opacity="0.7" />
<stop id="tmpM" offset="50%" stop-color="#2389BA" stop-opacity="0.7" />
<stop id="tmpL" offset="100%" stop-color="#2389BA" stop-opacity="0.7" />
</linearGradient>
<filter id="fillpartialEV" primitiveUnits="objectBoundingBox" x="-5%" y="-5%" width="110%"
height="110%">
<feFlood flood-color="#1f5a66" />
<feComposite id="evChargeState" y="100%" operator="in" in2="SourceGraphic" />
<feComposite operator="over" in2="SourceGraphic" />
</filter>
<filter id="fillpartialHeat" primitiveUnits="objectBoundingBox" x="-5%" y="-5%" width="110%"
height="110%">
<feFlood flood-color="#5E2B21" />
<feComposite id="heatChargeState" y="100%" operator="in" in2="SourceGraphic" />
<feComposite operator="over" in2="SourceGraphic" />
</filter>
<filter id="fillpartialBat" primitiveUnits="objectBoundingBox" x="-5%" y="-5%" width="110%"
height="110%">
<feFlood flood-color="#25481C" />
<feComposite id="batChargeState" y="100%" operator="in" in2="SourceGraphic" />
<feComposite operator="over" in2="SourceGraphic" />
</filter>
<filter id="fillpartialWater" primitiveUnits="objectBoundingBox" x="-5%" y="-5%" width="110%"
height="110%">
<feFlood flood-color="#182C43" />
<feComposite id="waterState" y="50%" operator="in" in2="SourceGraphic" />
<feComposite operator="over" in2="SourceGraphic" />
</filter>
<g id="heater" transform="rotate(-90),scale(0.07,0.07)">
<path
d="M485.924,134.508c14.381,0,26.076-11.669,26.076-26.017v-17.39c0-14.348-11.695-26.017-26.076-26.017H468.61v-8.737
c0-23.89-19.466-43.331-43.39-43.331s-43.39,19.441-43.39,43.331v8.737h-26.034v-8.737c0-23.89-19.466-43.331-43.39-43.331
c-23.924,0-43.39,19.441-43.39,43.331v8.737h-26.034v-8.737c0-23.89-19.466-43.331-43.39-43.331
c-23.924,0-43.39,19.441-43.39,43.331v8.737h-26.034v-8.737c0-23.89-19.466-43.331-43.39-43.331S43.39,32.458,43.39,56.348v8.737
H17.356V39.051H0v34.712v52.068v34.712h17.356v-26.034H43.39v269.017H8.678c-4.797,0-8.678,3.881-8.678,8.678v34.712
c0,4.797,3.881,8.678,8.678,8.678c4.797,0,8.678-3.881,8.678-8.678v-26.034H43.39v34.78c0,23.89,19.466,43.322,43.39,43.322
s43.39-19.432,43.39-43.322v-0.068h26.034v0.068c0,23.89,19.466,43.322,43.39,43.322c23.924,0,43.39-19.432,43.39-43.322v-0.068
h26.034v0.068c0,23.89,19.466,43.322,43.39,43.322c23.924,0,43.39-19.432,43.39-43.322v-0.068h26.034v0.068
c0,23.89,19.466,43.322,43.39,43.322s43.39-19.432,43.39-43.322v-0.068h17.314c14.381,0,26.076-11.669,26.076-26.017v-17.39
c0-14.347-11.695-26.017-26.076-26.017H468.61V134.508H485.924z M468.61,82.441h17.314c4.805,0,8.72,3.881,8.72,8.661v17.39
c0,4.78-3.915,8.661-8.72,8.661H468.61V82.441z M43.39,117.153H17.356V82.441H43.39V117.153z M112.814,73.763v52.068v269.017
v52.068v8.746c0,14.313-11.678,25.966-26.034,25.966s-26.034-11.653-26.034-25.966V125.831V73.763V56.348
c0-14.322,11.678-25.975,26.034-25.975s26.034,11.653,26.034,25.975V73.763z M156.203,438.237h-26.034v-34.712h26.034V438.237z
M156.203,386.169h-26.034V134.508h26.034V386.169z M156.203,117.153h-26.034V82.441h26.034V117.153z M225.627,73.763v52.068
v269.017v52.068v8.746c0,14.313-11.678,25.966-26.034,25.966s-26.034-11.653-26.034-25.966v-8.746v-52.068V125.831V73.763V56.348
c0-14.322,11.678-25.975,26.034-25.975s26.034,11.653,26.034,25.975V73.763z M269.017,438.237h-26.034v-34.712h26.034V438.237z
M269.017,386.169h-26.034V134.508h26.034V386.169z M269.017,117.153h-26.034V82.441h26.034V117.153z M338.441,73.763v52.068
v269.017v52.068v8.746c0,14.313-11.678,25.966-26.034,25.966s-26.034-11.653-26.034-25.966v-8.746v-52.068V125.831V73.763V56.348
c0-14.322,11.678-25.975,26.034-25.975s26.034,11.653,26.034,25.975V73.763z M381.831,438.237h-26.034v-34.712h26.034V438.237z
M381.831,386.169h-26.034V134.508h26.034V386.169z M381.831,117.153h-26.034V82.441h26.034V117.153z M451.254,73.763v52.068
v269.017v52.068v8.746c0,14.313-11.678,25.966-26.034,25.966s-26.034-11.653-26.034-25.966v-8.746v-52.068V125.831V73.763V56.348
c0-14.322,11.678-25.975,26.034-25.975s26.034,11.653,26.034,25.975V73.763z M485.924,403.525c4.805,0,8.72,3.881,8.72,8.661
v17.39c0,4.78-3.915,8.661-8.72,8.661H468.61v-34.712H485.924z" />
</g>
<g id="gas">
<path fill-opacity="1" stroke-width="0.2" stroke-linejoin="round"
d="M 42.75,19L 42.75,31.4977C 44.5834,31.378 48.9184,32.7881 48.2917,49.0833C 49.0833,53.8333 54.625,53.8333 55.8125,47.5C 57,42.75 53.8333,42.75 52.25,28.5C 52.25,26.9167 52.25,22.1667 45.9167,19L 45.9167,17.4167C 52.25,17.4167 57,25.3333 57,26.9167C 57,28.5 55.4167,28.5 55.4167,28.5C 55.4167,28.5 55.8125,38 57.3958,39.5833C 58.9792,41.1667 58.5833,45.9167 58.5833,49.0833C 58.5833,52.25 53.8333,55.4167 52.25,55.4167C 50.6667,55.4167 45.9167,55.4167 45.9167,50.6667C 45.9167,46.3403 45.2599,36.7598 42.75,35.0854L 42.75,55.4167C 43.6244,55.4167 44.3333,56.1255 44.3333,57L 44.3333,60.1667L 19,60.1667L 19,57C 19,56.1255 19.7089,55.4167 20.5833,55.4167L 20.5833,19C 20.5833,17.2511 22.0011,15.8333 23.75,15.8333L 39.5833,15.8333C 41.3322,15.8333 42.75,17.2511 42.75,19 Z M 25.7291,33.2501L 37.6041,33.25C 38.4786,33.25 39.5833,32.1453 39.5833,31.2709L 39.5833,20.9792C 39.5833,20.1048 38.4786,19 37.6041,19L 25.7292,19C 24.8547,19 23.75,20.1048 23.75,20.9792L 23.75,31.2709C 23.75,32.1453 24.8547,33.2501 25.7291,33.2501 Z " />
</g>
<g id="water">
<path
d="M283.897,92.846c-36.582-49.345-73.688-89.267-74.061-89.664C207.944,1.153,205.296,0,202.523,0
c-2.774,0-5.423,1.152-7.314,3.182c-0.371,0.397-37.478,40.319-74.06,89.664c-49.971,67.403-75.308,119.726-75.308,155.513
c0,86.396,70.287,156.688,156.682,156.688c86.396,0,156.683-70.29,156.683-156.688C359.206,212.572,333.868,160.25,283.897,92.846z
M218.171,354.342c-8.213,1.941-16.68,2.926-25.162,2.926c-60.294,0-109.347-49.055-109.347-109.35
c0-8.312,2.559-23.373,14.75-47.914c1.225-2.467,4.046-3.691,6.687-2.908c2.639,0.785,4.33,3.357,4.007,6.091
c-0.28,2.361-0.421,4.584-0.421,6.607c0,64.629,45.966,120.77,109.298,133.484c2.607,0.525,4.5,2.795,4.545,5.455
C222.575,351.396,220.761,353.729,218.171,354.342z" />
</g>
<g id="raisedBedIcon" filter="url(#shadow)">
<path d="M2 8h20v12H2V8Z" fill="none" stroke="currentColor" stroke-width="1.8" />
<path d="M4 12h16M5 16h14" fill="none" stroke="currentColor" stroke-width="1.4" />
<path d="M7 8V4m0 2L5 4m2 2 2-2M13 8V3m0 3-2-2m2 2 2-2M18 8V4m0 2-2-2m2 2 2-2" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" />
</g>
<g id="flowerPotIcon" filter="url(#shadow)">
<path d="M3 8h18l-2 4H5L3 8Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" />
<path d="M6 12h12l-2 8H8l-2-8Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" />
<path d="M12 8V3m0 3L9 4m3 2 3-2" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" />
</g>
<g id="gardenPlantIcon" filter="url(#shadow)">
<path d="M12 19V10m0 4-4-3m4 1 4-4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
<path d="M12 3C9 3 7 5 7 8c-3 0-5 2-5 5s2 5 5 5h10c3 0 5-2 5-5s-2-5-5-5c0-3-2-5-5-5Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" />
<path d="M5 22h14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
</g>
<g id="voltage">
<path stroke="#212529" stroke-width="0.2px" d="M11.46,18V13.58H9.86L12.54,8v3.89h1.6Z" />
</g>
<path id="powerTextPath" d="M 100, 100 m -87, 0 a 87,87 0 1,1 174,0 a 87,87 0 1,1 -174,0"></path>
<path id="infoTextPath" d="M 100, 100 m -105, 0 a 105,105 0 1,0 210,0 a 105,105 0 1,0 -210,0"></path>
<g id="pvPlate" fill="none" stroke-width="4" stroke="#f7c000">
<rect width="70" height="40" x="0" y="0" rx="5" ry="5" />
</g>
<g id="ElementBG">
<path fill="#212529" stroke-width="1.5"
d="M 99.80277789465349 -12.999827891201505 A 113 113 0 1 0 100 -13" />
<path fill="none" stroke-width="4"
d="M 99.86386438745993 22.00011880076356 A 78 78 0 1 0 100 22" />
</g>
<g id="plugIcon" filter="url(#shadow)" transform="translate(30,27),scale(6,6)" fill="#71afcd">
<path d="M15.21,6.73V2.63A.72.72,0,0,0,14.4,2h0a.71.71,0,0,0-.75.66V6.73Z" />
<path d="M10.39,6.73V2.62A.7.7,0,0,0,9.6,2h0a.73.73,0,0,0-.78.66V6.73Z" />
<path
d="M6.71,7.23C6.37,7.22,6,8,6,8.8V10c0,2.48.57,4.62,1.63,5.28S9,16,10.35,16.77V22h3.3V16.77c1.38-.8,1.66-.85,2.72-1.51S18,12.46,18,10V8.8c0-.78-.37-1.58-.71-1.57Z" />
</g>
<g id="ogIcon" filter="url(#shadow)" transform="translate(52,45),scale(4,4)">
<polygon fill="#70CCBB"
points="12.01 1.5 2 10.32 3.11 10.32 3.11 21.5 5.12 21.5 5.12 10.32 12 4.13 18.89 10.32 18.89 21.5 20.89 21.5 20.89 10.32 22 10.32 12.01 1.5" />
<path fill="#70CCBB"
d="M8.55,12.65v3.49a2.12,2.12,0,0,0,2.13,2.09h.67s0,3.23,0,3.27h1.49s0-3.24,0-3.28h.77a1.82,1.82,0,0,0,1.8-1.85l0-3.73" />
<path fill="#70CCBB" d="M10.76,11.41v-1.3c0-.45-.17-.67-.52-.68s-.54.22-.54.69v1.67h1.06Z" />
<path fill="#70CCBB"
d="M14.3,11.41c0-.44,0-.88,0-1.3s-.17-.67-.51-.68-.55.22-.55.69v1.67H14.3Z" />
<path fill="#212529"
d="M9.86,13.86h4.28a0,0,0,0,1,0,0v2a1,1,0,0,1-1,1H10.86a1,1,0,0,1-1-1v-2A0,0,0,0,1,9.86,13.86Z" />
</g>
<g id="heatIcon" filter="url(#shadow)" transform="translate(52,70),scale(4,4)" fill="#C2614E">
<path clip-path="M-22 2.24h42V22h-42z"
d="M16.543 8.028c-.023 1.503-.523 3.538-2.867 4.327.734-1.746.846-3.417.326-4.979-.695-2.097-3.014-3.735-4.557-4.627-.527-.306-1.203.074-1.193.683.02 1.112-.318 2.737-1.959 4.378C4.107 9.994 3 12.251 3 14.517 3 17.362 5 21 9 21c-4.041-4.041-1-7.483-1-7.483C8.846 19.431 12.988 21 15 21c1.711 0 5-1.25 5-6.448 0-3.133-1.332-5.511-2.385-6.899-.347-.458-1.064-.198-1.072.375" />
</g>
<g id="egIcon" filter="url(#shadow)" transform="translate(52,50),scale(4,4)">
<rect stroke="#70CC9C" stroke-width="2" fill="none" x="3" y="3" width="18" height="18" rx="2" />
<path fill="#70CC9C"
d="M8.55,9.2v3.47a2.11,2.11,0,0,0,2.13,2.08h.67s0,3.22,0,3.26h1.49s0-3.22,0-3.26h.77a1.81,1.81,0,0,0,1.8-1.84l0-3.71" />
<path fill="#70CC9C" d="M10.76,8V6.67c0-.45-.17-.66-.52-.67s-.54.22-.54.68V8.34h1.06Z" />
<path fill="#70CC9C" d="M14.3,8c0-.44,0-.87,0-1.3S14.12,6,13.78,6s-.55.22-.55.68V8.34H14.3Z" />
<path fill="#212529"
d="M9.86,10.41h4.28a0,0,0,0,1,0,0v2a1,1,0,0,1-1,1H10.86a1,1,0,0,1-1-1v-2A0,0,0,0,1,9.86,10.41Z" />
</g>
<g id="ugIcon" filter="url(#shadow)" transform="translate(52,50),scale(4,4)">
<path
style="fill:none;stroke:#83a55b;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"
d="M 2.977125,13.982235 V 2.973455 h 17.952227 l 0.03597,16.261337"
id="path12" />
<path fill="#83A55B"
d="M8.55,9.2v3.47a2.11,2.11,0,0,0,2.13,2.08h.67s0,3.22,0,3.26h1.49s0-3.22,0-3.26h.77a1.81,1.81,0,0,0,1.8-1.84l0-3.71" />
<path fill="#83A55B" d="M10.76,8V6.67c0-.45-.17-.66-.52-.67s-.54.22-.54.68V8.34h1.06Z" />
<path fill="#83A55B" d="M14.3,8c0-.44,0-.87,0-1.3S14.12,6,13.78,6s-.55.22-.55.68V8.34H14.3Z" />
<path fill="#212529"
d="M9.86,10.41h4.28a0,0,0,0,1,0,0v2a1,1,0,0,1-1,1H10.86a1,1,0,0,1-1-1v-2A0,0,0,0,1,9.86,10.41Z" />
<path
style="fill:none;stroke:#83a55b;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;"
d="M 23.96101,19.98372 C 11.24143,20.28899 11.139675,15.0994 -3.106255,14.081835" />
</g>
<g id="carIcon" filter="url(#shadow)" transform="translate(45,45),scale(4.8,4.8)"
fill="#212529" stroke-linecap="round">
<path d="M16.22,15.37l-9.3-.16L3,15,.88,14.47.75,10.39A2.78,2.78,0,0,1,1.31,9.2L2.55,7.63" />
<path
d="M.78,7.46,7,7l.87,0L11.37,7a5.57,5.57,0,0,1,1.06.23,9.43,9.43,0,0,1,2.16,1L17.23,9.9l4.22.7a1.73,1.73,0,0,1,1.21.93l.26.49a2.84,2.84,0,0,1,.32,1.6l0,.23A1.52,1.52,0,0,1,22.53,15h0a1.53,1.53,0,0,1-.56.21l-.89.15H15.59" />
<path fill="#888888" stroke="none"
d="M16.06,11c-1.85.11-3.7.14-5.55.16a13.36,13.36,0,0,1-2.82,0,2.62,2.62,0,0,1-.87-.4,1.28,1.28,0,0,1-.53-1.05,1.4,1.4,0,0,1,.09-.51,2.67,2.67,0,0,1,.22-.35,2.79,2.79,0,0,1,.5-.58s.09,0,.11,0a.06.06,0,0,1,0,.05h0c0,.25,0,.49,0,.71s0,.23,0,.31a.75.75,0,0,0,.08.18c.07.09.17.14.17.08a1.81,1.81,0,0,0,.43.05,14.41,14.41,0,0,1,2.68.33c1.82.26,3.64.53,5.46.88a.08.08,0,0,1,.06.09A.07.07,0,0,1,16.06,11Z" />
<circle cx="4.94" cy="15.08" r="1.98" />
<circle cx="19.19" cy="15.08" r="1.98" />
</g>
<g id="plugCordIcon" transform="scale(0.08,0.08),rotate(340),translate(-350,1200)">
<path
d="M489.8,280.92c0-13.9-11.3-25.2-25.2-25.2h-74.1c-62.1,0-112.4,50.2-112.7,112.2c-0.1,32.3-23,61-55,65
c-38.4,4.8-71.2-25.2-71.2-62.6v-56.4c57.6-11.7,101-62.6,101-123.7v-64c0-9.1-7.3-16.4-16.4-16.4h-31.3v-79.1
c0-12.4-9-23.4-21.3-24.6c-14.1-1.4-26,9.7-26,23.6v80.1H95.2v-79.1c0-12.4-9-23.4-21.4-24.6c-14.1-1.3-26,9.7-26,23.6v80.1H16.4
c-9.1,0-16.4,7.3-16.4,16.4v64c0,61.1,43.4,112,101,123.7v53.5c0,61.5,47.7,114,109.2,116.3c64.6,2.4,118-49.4,118-113.5v-0.9
c0-34.9,28.3-63.1,63.1-63.1h73.2C478.5,306.12,489.8,294.82,489.8,280.92L489.8,280.92z" />
</g>
<g id="lockIcon" transform="translate(80,130),scale(0.11,0.11)" style="">
<path
d="M65,330h200c8.284,0,15-6.716,15-15V145c0-8.284-6.716-15-15-15h-15V85c0-46.869-38.131-85-85-85
S80,38.131,80,85v45H65c-8.284,0-15,6.716-15,15v170C50,323.284,56.716,330,65,330z M207.481,219.356l-42.5,42.5
c-2.929,2.929-6.768,4.394-10.606,4.394s-7.678-1.465-10.606-4.394l-21.25-21.25c-5.858-5.858-5.858-15.354,0-21.213
c5.857-5.858,15.355-5.858,21.213,0l10.644,10.643l31.894-31.893c5.857-5.858,15.355-5.858,21.213,0
C213.34,204.002,213.34,213.498,207.481,219.356z M110,85c0-30.327,24.673-55,55-55s55,24.673,55,55v45H110V85z"
style="stroke-width:15px;stroke:rgb(30,30,40);fill:#778;" />
</g>
<g filter="url(#shadow)" id="clock" style="fill:#cc0;"
transform="translate(80,32),scale(0.07,0.07)">
<path
d="m256,51.8c-112.6,0-204.2,91.6-204.2,204.2s91.6,204.2 204.2,204.2 204.2-91.6 204.2-204.2-91.6-204.2-204.2-204.2v-1.42109e-14zm0,449.2c-135.1,0-245-109.9-245-245s109.9-245 245-245 245,109.9 245,245-109.9,245-245,245z" />
<path
d="m327.9,276.4h-71.9c-11.3,0-20.4-9.1-20.4-20.4v-132.2c0-11.3 9.1-20.4 20.4-20.4 11.3,0 20.4,9.1 20.4,20.4v111.8h51.5c11.3,0 20.4,9.1 20.4,20.4s-9.1,20.4-20.4,20.4z" />
</g>
<g id="sunIcon" filter="url(#shadow)" transform="translate(30,27),scale(6,6)"
style="stroke:#f7c000;stroke-width:2;stroke-linecap:round;">
<path id="Pfad_7565" data-name="Pfad 7565" fill="#f7c002" stroke-width="0"
d="M12,8a4,4,0,1,0,4,4A4,4,0,0,0,12,8Z" />
<line x1="12" y1="3" x2="12" y2="5" />
<line x1="12" y1="19" x2="12" y2="21" />
<line x1="19" y1="12" x2="21" y2="12" />
<line x1="3" y1="12" x2="5" y2="12" />
<line x1="5.7" y1="18.5" x2="7" y2="17" />
<line x1="5.7" y1="5.7" x2="7" y2="7" />
<line x1="18.5" y1="18.5" x2="16.7" y2="17" />
<line x1="18.5" y1="5.7" x2="16.7" y2="7" />
</g>
<g id="battIcon" filter="url(#shadow)" transform="translate(30,25),scale(6,6)">
<g>
<rect fill="#6cbe58" x="9.25" y="3" width="5.5" height="4" rx="1" ry="1" />
<path fill="#6cbe58"
d="M16.28,5H7.72A1.21,1.21,0,0,0,6.5,6.2V19.73A1.28,1.28,0,0,0,7.72,21h8.56a1.28,1.28,0,0,0,1.22-1.27V6.2A1.21,1.21,0,0,0,16.28,5Z" />
<rect id="discharged" fill="#212529" rx="1" ry="1" x="8" y="6.5" width="8"
height="13" />
<use id="charge" fill="#cccc00" href="#voltage" />
</g>
</g>
<g id="gridIcon" filter="url(#shadow)" transform="translate(30,27),scale(6,6)">
<path fill="#808090"
d="M13.38,8.54V6.63H18.3V8.06h.95V5.7l-6.35-1L12,2l-1,2.8-6.25.9V8.07H5.6V6.63h4.84V8.54l-7.59,1.9v2.84H3.8V11.76H9.87L6.93,22H9.11l.57-2.46h4.59L14.8,22h2.27L14.13,11.76H20.2v1.52h.95V10.44Zm-3.32,8.81L12,11.11l1.91,6.24Z" />
</g>
<g id="inverterIcon" filter="url(#shadowBig)" fill="#b2b2b2" stroke-width="2px" stroke="#666"
transform="translate(43,43),scale(1.6,1.6)">
<rect stroke-width="2px" x="5.65" y="3" width="58.7" height="64" rx="6.8" />
<circle cx="35.03" cy="29.61" r="11" />
<circle cx="15" cy="40" r="4" />
<path
d="M40.19,52H36.73a2,2,0,0,0-3.46,0H29.81a1.08,1.08,0,1,0,0,2.15h3.46a2,2,0,0,0,3.46,0h3.46a1.08,1.08,0,1,0,0-2.15Z" />
<path fill="#e2001a"
d="M34.89,61.87c-3.24,0-5.87-.73-5.87-1.62s2.63-1.64,5.87-1.64,5.87.74,5.87,1.64S38.13,61.87,34.89,61.87Z" />
<line x1="5.07" y1="53.08" x2="28.73" y2="53.08" />
<line x1="41.27" y1="53.08" x2="64.93" y2="53.08" />
</g>
<g id="northIcon" fill="#808090">
<path
d="M42.066 0v20h3.752V6.957L53.881 20h4.053V0h-3.752v13.355L45.996 0zm5.57 26.688l-25.77 69.949c-.823 2.238 1.658 4.248 3.677 2.978L50 84.195l24.455 15.42c2.02 1.273 4.504-.738 3.68-2.978l-25.79-70c-.472-1.096-1.283-1.632-2.384-1.635c-1.1-.003-2.017.856-2.324 1.686zm-.136 14.83V79.86L29.1 91.463z"></path>
</g>
<g filter="url(#shadow)" id="ecoIcon" transform="translate(60,12)">
<path style="stroke:#6cbe58;fill:rgb(30,30,40);" stroke-width="3" stroke-linejoin="round"
d="M 57,22.1667C 49.0833,28.5 52.25,31.6667 50.6667,36.4167C 49.0833,41.1667 50.6667,42.75 44.3333,47.5C 36.2227,53.583 29.2917,49.875 29.2917,49.875C 27.7083,51.4583 21.7708,54.2292 21.7708,54.2292L 20.0143,50.7285C 26.3476,49.1452 27.7083,47.1042 27.7083,47.1042C 26.125,43.9375 23.75,38 28.5,30.0833C 33.25,22.1667 57,22.1667 57,22.1667 Z M 30.0833,44.3333L 31.6667,45.9167C 31.6667,45.9167 34.8333,44.3333 36.4167,39.5833C 38,34.8333 45.9167,26.9167 45.9167,26.9167C 42.75,28.5 36.4167,34.8333 34.8333,39.5833C 33.25,44.3333 30.0833,44.3333 30.0833,44.3333 Z " />
</g>
<g filter="url(#shadow)" id="alertIcon" fill="#aa0" transform="translate(34,28),scale(2.2,2.2)">
<path
d="M214.413,746a4.455,4.455,0,0,1-3.84-2.166,4.249,4.249,0,0,1,0-4.334l25.572-43.331a4.483,4.483,0,0,1,7.679,0L269.4,739.5a4.249,4.249,0,0,1,0,4.334,4.452,4.452,0,0,1-3.84,2.166H214.413ZM240,706a4,4,0,0,0-4,4v16a4,4,0,0,0,8,0V710A4,4,0,0,0,240,706Zm0,36a4,4,0,1,0-4-4A4,4,0,0,0,240,742Z"
id="attention" transform="translate(-209.969 -694)" />
</g>
</defs>
<g id="pvInfo" class="clickable" transform="translate(330,280)">
<animateMotion begin="0.5s" dur="2s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L-250 -250" fill="freeze">
</animateMotion>
<animate id="openDetails" begin="click" attributetype="CSS" attributeName="opacity" from="1"
to="1"
dur="0.1s" repeatCount="1" calcMode="spline"
fill="freeze">
</animate>
<!--<g filter="url(#glow)">-->
<use href="#ElementBG" stroke="#574400" />
<use href="#sunIcon" />
<path id="pvArc" fill="none" stroke="#f7c000" stroke-width="30" />
<path id="pv2Arc" fill="none" stroke="#f78d00" stroke-width="30" />
<path id="pv3Arc" fill="none" stroke="#f72300" stroke-width="30" />
<use href="#circGrid" style="stroke:#574400;" />
<!--</g>-->
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="pvText" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
<textPath id="pv3txt" href="#infoTextPath" startOffset="12%">--.-- kW</textPath>
<textPath id="pv2txt" href="#infoTextPath" startOffset="35%">--.-- kW</textPath>
<textPath id="pv1txt" href="#infoTextPath" startOffset="55%">--W</textPath>
</text>
</g>
<circle id="closeDetails" fill="transparent" transform="translate(250,124)">
<animate begin="click" attributetype="CSS" attributeName="r" from="120" to="0" dur="0.1s"
repeatCount="1"
fill="freeze" />
<animate begin="pvInfo.click" attributeName="r" from="0" to="120" dur="0.1s" repeatCount="1"
fill="freeze" />
</circle>
<!--NON-PV-->
<g id="nonPVGroup">
<animate begin="pvInfo.click" attributetype="CSS" attributeName="opacity" from="1.0" to="0.0"
dur="1s"
repeatCount="1" calcMode="spline" keyTimes="0;1" keySplines=".1,0,.27,1" fill="freeze" />
<animate begin="closeDetails.click" attributetype="CSS" attributeName="opacity" from="0.0"
to="1.0"
dur="1s" repeatCount="1" calcMode="spline" keyTimes="0;1" keySplines=".1,0,.27,1"
fill="freeze" />
<g transform="translate(330,280), scale(0.90,0.90)" id="heatInfo" class="clickable" onclick="openModal('heater')">
<animateMotion begin="0.5s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L340 0" fill="freeze"></animateMotion>
<animateMotion begin="1.2s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".6,0,.27,1" path="M340 0 L700 0" fill="freeze"></animateMotion>
<!--<g
filter="url(#glow)">-->
<use href="#ElementBG" stroke="#5E2B21" />
<path fill="url(#gradient1)" stroke="#5E2B21" stroke-width="4"
d="M 99.86386438745993 22.00011880076356 A 78 78 0 1 0 100 22" />
<use href="#heatIcon" />
<path id="heatArc" fill="none" stroke="#C2614E" stroke-width="30" />
<use href="#circGrid6kW" style="stroke:#5E2B21;" />
<!--</g>-->
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="heatText" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
<textPath id="heatUp" href="#infoTextPath" startOffset="9%">--°C</textPath>
<textPath id="heatDown" href="#infoTextPath" startOffset="36%">--°C</textPath>
</text>
<use id="heatEco" href="#ecoIcon" style="display:none;" />
<use id="heatDefault" href="#alertIcon" style="display:none;" />
</g>
<g transform="translate(330,280), scale(0.90,0.90)" class="clickable" id="CarInfoOG" onclick="openModal('CarOG')">
<animateMotion begin="0.5s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L340 0" fill="freeze" />
<animateMotion id="lastInitAni" begin="1.2s" dur=".7s" repeatCount="1" calcMode="spline"
keyTimes="0;1" keySplines=".6,0,.27,1" path="M340 0 L800 -300" fill="freeze" />
<!--<g
filter="url(#glow)">-->
<use href="#ElementBG" stroke="#182C43" />
<!--<path
filter="url(#fillpartialEV)" fill="#212529" stroke="#642445" stroke-width="4" d="M 99.863863
22.00012 A 78 78 0 1 0 100 22" />-->
<use href="#carIcon" stroke="#4b71a6" />
<use id="evChargeOG" href="#voltage" transform="translate(167,45),rotate(90),scale(5,5)"
style="fill:#cccc00; display:none;" />
<path id="evArcOG" fill="none" stroke="#4b71a6" stroke-width="30" />
<use href="#circGrid" style="stroke:#182C43;" />
<!--</g>-->
<use id="evPlugOG" href="#plugCordIcon" style="fill:#4b71a6;display:none;" />
<!--<use
href="#voltage" style="fill:#888;" transform="scale(3, 3),translate(7,32), rotate(50)" />-->
<!--<use
href="#gas" style="fill:#888;" transform="scale(0.55, 0.55),translate(210,319), rotate(337)"
/>-->
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="evTextOG" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
<!--<textPath
id="evFuelOG" href="#infoTextPath" startOffset="40%">-%</textPath>-->
<!--<textPath
id="evSOCOG" href="#infoTextPath" startOffset="15%">-%</textPath>-->
</text>
<!--<use
id="evLockOG" href="#lockIcon" style="display:none;" />-->
<use id="evEcoOG" href="#ecoIcon" style="display:none;" />
<use id="evDefaultOG" href="#plugCordIcon" transform="translate(105,-65),rotate(20)"
style="fill:#a53;display:none;" />
<use id="evNextTripOG" href="#clock" style="display:none;" />
<text dy="160" dx="100" id="evRemText" class="powerTextClass" />
</g>
<g transform="translate(330,280), scale(0.90,0.90)" class="clickable" id="CarInfo" onclick="openModal('CarEG')">
<animateMotion begin="0.5s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L340 0" fill="freeze" />
<animateMotion id="lastInitAni" begin="1.2s" dur=".7s" repeatCount="1" calcMode="spline"
keyTimes="0;1" keySplines=".6,0,.27,1" path="M340 0 L800 300" fill="freeze" />
<!--<g
filter="url(#glow)">-->
<use href="#ElementBG" stroke="#1f5a66" />
<path filter="url(#fillpartialEV)" fill="#212529" stroke="#1f5a66" stroke-width="4"
d="M 99.863863 22.00012 A 78 78 0 1 0 100 22" />
<use href="#carIcon" stroke="#60afbf" />
<use id="evCharge" href="#voltage" transform="translate(167,45),rotate(90),scale(5,5)"
style="fill:#cccc00; display:none;" />
<path id="evArc" fill="none" stroke="#60afbf" stroke-width="30" />
<use href="#circGrid" style="stroke:#1f5a66;" />
<use id="evPlug" href="#plugCordIcon" style="fill:#60afbf;display:none;" />
<!--</g>-->
<use href="#voltage" style="fill:#888;" transform="scale(3, 3),translate(7,32), rotate(50)" />
<use href="#gas" style="fill:#888;"
transform="scale(0.55, 0.55),translate(210,319), rotate(337)" />
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="evText" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
<textPath id="evFuel" href="#infoTextPath" startOffset="40%">--%</textPath>
<textPath id="evSOC" href="#infoTextPath" startOffset="15%">--%</textPath>
</text>
<use id="evLock" href="#lockIcon" style="display:none;" />
<use id="evEco" href="#ecoIcon" style="display:none;" />
<use id="evDefault" href="#plugCordIcon" transform="translate(105,-65),rotate(20)"
style="fill:#a53;display:none;" />
<use id="evNextTrip" href="#clock" style="display:none;" />
<text dy="160" dx="100" id="evRemText" class="powerTextClass" />
</g>
<g transform="translate(330,280), scale(0.90,0.90)" id="UGInfo">
<animateMotion begin="0.5s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L340 0" fill="freeze" />
<animateMotion begin="1.2s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".6,0,.27,1" path="M340 0 L180 300" fill="freeze" />
<!--<g
filter="url(#glow)">-->
<use href="#ElementBG" stroke="#314619" />
<use href="#ugIcon" />
<path id="ugArc" fill="none" stroke="#83A55B" stroke-width="30" />
<use href="#circGrid" style="stroke:#314619;" />
<!--</g>-->
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="ugText" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
</text>
</g>
<g transform="translate(330,280), scale(0.90,0.90)" id="EGInfo">
<animateMotion begin="0.5s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L340 0" fill="freeze" />
<animateMotion begin="1.2s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".6,0,.27,1" path="M340 0 L500 300" fill="freeze" />
<!--<g
filter="url(#glow)">-->
<use href="#ElementBG" stroke="#18423B" />
<use href="#egIcon" />
<path id="egArc" fill="none" stroke="#70CC9C" stroke-width="30" />
<use href="#circGrid" style="stroke:#18423B;" />
<!--</g>-->
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="egText" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
</text>
<use id="heatEG" transform="translate(120,170)"
style='display:none;mix-blend-mode: difference;fill:#70CCBB;' href="#heater" />
</g>
<g transform="translate(330,280), scale(0.90,0.90)" id="waterInfo" class="clickable" onclick="openModal('watering')">
<animateMotion begin="0.5s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L340 0" fill="freeze" />
<animateMotion begin="1.2s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".6,0,.27,1" path="M340 0 L120 -300" fill="freeze" />
<use href="#ElementBG" stroke="#182C43" />
<path filter="url(#fillpartialWater)" fill="#212529" stroke="#182C43" stroke-width="4"
d="M 99.863863 22.00012 A 78 78 0 1 0 100 22" />
<use href="#water" transform="translate(60,55), scale(0.20,0.20)" fill="#3A6292" />
<!-- Wasserstand und Temperatur werden von der Bewässerungssteuerung nicht unterstützt.
<text id="waterText" filter="url(#shadowMiddle)" class="powerTextClass" text-anchor="middle" x="100" y="55">-.- cm</text>
<text id="waterTemp" filter="url(#shadowMiddle)" class="powerTextClass" text-anchor="middle" x="100" y="88">-.- °C</text>
-->
<use id="wateringFrontIcon" href="#gardenPlantIcon" transform="translate(152,162),rotate(310),scale(1.3)" fill="#6c757d" color="#6c757d">
<title>Vorn: aus</title>
</use>
<use id="wateringTroughIcon" href="#flowerPotIcon" transform="translate(18,100),rotate(75),scale(1.3)" fill="#6c757d" color="#6c757d">
<title>Tröge: aus</title>
</use>
<use id="wateringBedsIcon" href="#raisedBedIcon" transform="translate(66,174),rotate(15),scale(1.3)" fill="#6c757d" color="#6c757d">
<title>Beete: aus</title>
</use>
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="wateringFrontStatus" href="#infoTextPath" startOffset="46%">Aus </textPath>
<textPath id="wateringTroughStatus" href="#infoTextPath" startOffset="10%">Aus </textPath>
<textPath id="wateringBedsStatus" href="#infoTextPath" startOffset="28%">Aus </textPath>
</text>
</g>
<g transform="translate(330,280), scale(0.90,0.90)" id="OGInfo">
<animateMotion begin="0.5s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L340 0" fill="freeze" />
<animateMotion begin="1.2s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".6,0,.27,1" path="M340 0 L500 -300" fill="freeze" />
<use href="#ElementBG" stroke="#18423B" />
<use href="#ogIcon" />
<path id="ogArc" fill="none" stroke="#70CCBB" stroke-width="30" />
<use href="#circGrid" style="stroke:#18423B;" />
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="ogText" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
</text>
<use id="heatOG" transform="translate(120,165)"
style='display:none;mix-blend-mode: difference;fill:#70CCBB;' href="#heater" />
</g>
<g transform="translate(330,280)">
<animateMotion begin="0.5s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L340 0" fill="freeze" />
<!--<g
filter="url(#glow)">-->
<use href="#ElementBG" stroke="#1D4154" />
<use href="#plugIcon" />
<path id="consumerArc" fill="none" stroke="#71afcd" stroke-width="30" />
<use href="#circGrid" style="stroke:#1D4154;" />
<!--</g>-->
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="consumerText" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
<textPath id="consumerTextAllg" href="#infoTextPath" startOffset="25%">--.-- kW</textPath>
<!--<textPath id="consumerTextEG" href="#infoTextPath" startOffset="35%">.- kW</textPath>
<textPath id="consumerTextUG" href="#infoTextPath" startOffset="55%">-.- kW</textPath> -->
</text>
</g>
<g transform="translate(330,280)">
<animateMotion begin="0.5s" dur="1.5s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L-250 250" fill="freeze" />
<!--<g
filter="url(#glow)">-->
<use href="#ElementBG" stroke="#25481C" />
<path id="batArc" fill="none" stroke="#6cbe58" stroke-width="30" />
<path filter="url(#fillpartialBat)" fill="#212529" stroke="#25481C" stroke-width="4"
d="M 99.863863 22.00012 A 78 78 0 1 0 100 22" />
<use href="#battIcon" />
<use href="#circGrid" style="stroke:#25481C;" />
<use id="batLim" href="#clock" style="" transform="translate(4,40)" />
<!--</g>-->
<text id="batLimText" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(103,132)"></text>
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="batText" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
<textPath id="batSOC" href="#infoTextPath" startOffset="26%">--.- %</textPath>
</text>
</g>
<g transform="translate(330,280)">
<animateMotion begin="0.5s" dur="1s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L-330 0" fill="freeze" />
<!--<g
filter="url(#glow)">-->
<use href="#ElementBG" stroke="#2D2D34" />
<path id="gridArc" fill="none" stroke="#808090" stroke-width="30" />
<use href="#circGrid" style="stroke:#2D2D34;" />
<!--</g>-->
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="gridText" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
<textPath id="il1txt" href="#infoTextPath" startOffset="10%">--.- A</textPath>
<textPath id="il2txt" href="#infoTextPath" startOffset="25%">--.- A</textPath>
<textPath id="il3txt" href="#infoTextPath" startOffset="40%">--.- A</textPath>
</text>
<use href="#gridIcon" />
</g>
<g transform="translate(330,280), scale(1.1,1.1)" class="tooltip-trigger"
data-tooltip-text="The amazing pink box">
<!--<g
filter="url(#glow)">-->
<use href="#ElementBG" stroke="#2D2D34" />
<!--</g>-->
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="genText" href="#powerTextPath" startOffset="22%">Autarkie: </textPath>
<textPath id="genInfoText" href="#infoTextPath" startOffset="25%"></textPath>
</text>
<use href="#inverterIcon" />
</g>
</g>
<g id="pvDetail" opacity="0" transform="scale(0.1,0.1)">
<animate begin="openDetails.begin" attributetype="CSS" attributeName="opacity" from="0" to="1.0"
dur="0.7s" repeatCount="1" calcMode="spline" keyTimes="0;1" keySplines=".1,0,.27,1"
fill="freeze" />
<animateTransform attributeName="transform"
type="scale"
from="0.1,0.1"
to="1,1"
begin="openDetails.begin"
dur="0.3s" repeatCount="1" fill="freeze" />
<animate begin="closeDetails.click" attributetype="CSS" attributeName="opacity" from="1.0"
to="0"
dur="0.4s" repeatCount="1" calcMode="spline" keyTimes="0;1" keySplines=".1,0,.27,1"
fill="freeze" />
<animateTransform attributeName="transform"
type="scale"
from="1,1"
to="0.1,0.1"
begin="closeDetails.click"
dur="0.7s" repeatCount="1" fill="freeze" />
<use href="#northIcon" transform="translate(1200,10),rotate(10)" />
<!--<polygon points="370 0, 1300 0, 1300 700, 0 700, 0 250, 370 250" fill="rgb(24, 27, 31)" />-->
<polygon points="560,80 720,80 720,300 1160,300 1160,370 560,370" stroke-linejoin="round"
fill="#f7c002" stroke-width="30" stroke="#f7c002" opacity="0.1" />
<use href="#pvPlate" transform="translate(600,80), rotate(90)" />
<use href="#pvPlate" transform="translate(600,150), rotate(90)" />
<use href="#pvPlate" transform="translate(600,220), rotate(90)" />
<use href="#pvPlate" transform="translate(600,290), rotate(90)" />
<use href="#pvPlate" transform="translate(640,80), rotate(90)" />
<use href="#pvPlate" transform="translate(640,150), rotate(90)" />
<use href="#pvPlate" transform="translate(640,220), rotate(90)" />
<use href="#pvPlate" transform="translate(640,290), rotate(90)" />
<use href="#pvPlate" transform="translate(680,80), rotate(90)" />
<use href="#pvPlate" transform="translate(680,150), rotate(90)" />
<use href="#pvPlate" transform="translate(680,220), rotate(90)" />
<use href="#pvPlate" transform="translate(680,290), rotate(90)" />
<use href="#pvPlate" transform="translate(720,80), rotate(90)" />
<use href="#pvPlate" transform="translate(720,150), rotate(90)" />
<use href="#pvPlate" transform="translate(720,220), rotate(90)" />
<use href="#pvPlate" transform="translate(790,290)" />
<use href="#pvPlate" transform="translate(720,330)" />
<use href="#pvPlate" transform="translate(790,330)" />
<use href="#pvPlate" transform="translate(870,290)" />
<use href="#pvPlate" transform="translate(870,330)" />
<use href="#pvPlate" transform="translate(940,330)" />
<use href="#pvPlate" transform="translate(1160,300), rotate(90)" />
<use href="#pvPlate" transform="translate(1120,300), rotate(90)" />
<use href="#pvPlate" transform="translate(1080,300), rotate(90)" />
<text id="det_pv0P" filter="url(#shadowBig)" class="infoTextClass" dx="55" dy="30"
transform="translate(560, 150), scale(1.5,1.5)">-.-- kW</text>
<polygon points="980,60 1160,60 1160,265 980,265" stroke-linejoin="round" fill="#F76D00"
stroke-width="30" stroke="#F76D00" opacity="0.1" />
<use href="#pvPlate" transform="translate(1160,200), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1115,200), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1070,200), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1160,130), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1115,130), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1070,130), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1025,130), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1160,60), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1115,60), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1070,60), rotate(90), scale(1,1.125)" />
<text id="det_pv1P" filter="url(#shadowBig)" class="infoTextClass" dx="55" dy="30"
transform="translate(990, 140), scale(1.5,1.5)">-.-- kW</text>
<use href="#pvPlate" x="200" y="680" />
<text id="det_pv33P" filter="url(#shadowMiddle)" class="infoTextClass" x="200" y="680" dx="36"
dy="26">#19</text>
<use href="#pvPlate" x="380" y="550" />
<text id="det_pv32P" filter="url(#shadowMiddle)" class="infoTextClass" x="380" y="550" dx="36"
dy="26">#19</text>
<use href="#pvPlate" x="380" y="590" />
<text id="det_pv31P" filter="url(#shadowMiddle)" class="infoTextClass" x="380" y="590" dx="36"
dy="26">#18</text>
<use href="#pvPlate" x="380" y="630" />
<text id="det_pv30P" filter="url(#shadowMiddle)" class="infoTextClass" x="380" y="630" dx="36"
dy="26">#17</text>
<use href="#pvPlate" x="310" y="510" />
<text id="det_pv29P" filter="url(#shadowMiddle)" class="infoTextClass" x="310" y="510" dx="36"
dy="26">#16</text>
<use href="#pvPlate" x="310" y="550" />
<text id="det_pv28P" filter="url(#shadowMiddle)" class="infoTextClass" x="310" y="550" dx="36"
dy="26">#15</text>
<use href="#pvPlate" x="310" y="590" />
<text id="det_pv27P" filter="url(#shadowMiddle)" class="infoTextClass" x="310" y="590" dx="36"
dy="26">#14</text>
<use href="#pvPlate" x="310" y="630" />
<text id="det_pv26P" filter="url(#shadowMiddle)" class="infoTextClass" x="310" y="630" dx="36"
dy="26">#13</text>
<use href="#pvPlate" x="240" y="510" />
<text id="det_pv25P" filter="url(#shadowMiddle)" class="infoTextClass" x="240" y="510" dx="36"
dy="26">#12</text>
<use href="#pvPlate" x="240" y="550" />
<text id="det_pv24P" filter="url(#shadowMiddle)" class="infoTextClass" x="240" y="550" dx="36"
dy="26">#11</text>
<use href="#pvPlate" x="240" y="590" />
<text id="det_pv23P" filter="url(#shadowMiddle)" class="infoTextClass" x="240" y="590" dx="36"
dy="26">#10</text>
<use href="#pvPlate" x="240" y="630" />
<text id="det_pv22P" filter="url(#shadowMiddle)" class="infoTextClass" x="240" y="630" dx="36"
dy="26">#9</text>
<use href="#pvPlate" x="170" y="510" />
<text id="det_pv20P" filter="url(#shadowMiddle)" class="infoTextClass" x="170" y="510" dx="36"
dy="26">#8</text>
<use href="#pvPlate" x="170" y="550" />
<text id="det_pv21P" filter="url(#shadowMiddle)" class="infoTextClass" x="170" y="550" dx="36"
dy="26">#7</text>
<use href="#pvPlate" x="170" y="590" />
<text id="det_pv19P" filter="url(#shadowMiddle)" class="infoTextClass" x="170" y="590" dx="36"
dy="26">#6</text>
<use href="#pvPlate" x="170" y="630" />
<text id="det_pv18P" filter="url(#shadowMiddle)" class="infoTextClass" x="170" y="630" dx="36"
dy="26">#5</text>
<use href="#pvPlate" x="100" y="510" />
<text id="det_pv14P" filter="url(#shadowMiddle)" class="infoTextClass" x="100" y="510" dx="36"
dy="26">#1</text>
<use href="#pvPlate" x="100" y="550" />
<text id="det_pv15P" filter="url(#shadowMiddle)" class="infoTextClass" x="100" y="550" dx="36"
dy="26">#2</text>
<use href="#pvPlate" x="100" y="590" />
<text id="det_pv16P" filter="url(#shadowMiddle)" class="infoTextClass" x="100" y="590" dx="36"
dy="26">#3</text>
<use href="#pvPlate" x="100" y="630" />
<text id="det_pv17P" filter="url(#shadowMiddle)" class="infoTextClass" x="100" y="630" dx="36"
dy="26">#4</text>
<use href="#pvPlate" transform="translate(600,500)" />
<text id="det_pv13P" filter="url(#shadowMiddle)" class="infoTextClass" x="600" y="500" dx="36"
dy="26">- W</text>
<use href="#pvPlate" transform="translate(500,500)" />
<text id="det_pv12P" filter="url(#shadowMiddle)" class="infoTextClass" x="500" y="500" dx="36"
dy="26">- W</text>
<use href="#pvPlate" transform="translate(460,120), rotate(-90)" />
<text id="det_pv5P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(460,120), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(460,190), rotate(-90)" />
<text id="det_pv4P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(460,190), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(460,270), rotate(-90)" />
<text id="det_pv9P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(460,270), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(460,340), rotate(-90)" />
<text id="det_pv8P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(460,340), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(460,420), rotate(-90)" />
<text id="det_pv11P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(460,420), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(460,490), rotate(-90)" />
<text id="det_pv10P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(460,490), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(400,120), rotate(-90)" />
<text id="det_pv7P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(400,120), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(400,190), rotate(-90)" />
<text id="det_pv6P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(400,190), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(400,270), rotate(-90)" />
<text id="det_pv3P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(400,270), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(400,340), rotate(-90)" />
<text id="det_pv2P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(400,340), rotate(-90)" dx="36" dy="26">- W</text>
<text id="invList" filter="url(#shadowMiddle)" class="tooltipTextClass"
transform="translate(80,255)"></text>
<rect width="305" height="210" x="70" y="250" rx="5" ry="5" fill="none" stroke-width="4px"
stroke="#f7c000" />
</g>
<g id="tooltip" visibility="hidden">
<rect x="2" y="2" width="200" height="60" fill="black" opacity="0.4" rx="2" ry="2" />
<rect width="200" height="60" fill="white" rx="2" ry="2" />
<text x="4" y="6">Tooltip</text>
</g>
</svg>
+170
View File
@@ -3431,3 +3431,173 @@ input[type='range']::-webkit-slider-runnable-track {
border-color: #00788F; border-color: #00788F;
color: var(--ton-schrift, #e8ede9); 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); } }
+33
View File
@@ -0,0 +1,33 @@
-- ---------------------------------------------------------------------------
-- Energiefluss-Uebersicht (Seite Solar, Karte "Realtime") - Datenbank `homeMesh`
-- ---------------------------------------------------------------------------
-- Welche Kreise es ueberhaupt gibt, woher ihre Werte kommen, wie sie heissen
-- und wo sie stehen, beschreibt der Katalog in js/solar/energieflussKatalog.js.
-- Hier steht nur, was ein Haus davon abweichend einstellt - unter
-- Einstellungen -> Uebersicht:
--
-- aktiv 0 blendet den Kreis samt seinen Fluessen aus. Haengen andere
-- daran (Wallbox OG am Obergeschoss), rutschen sie eine Ebene
-- hoeher (an den Verbrauch).
-- name eigene Beschriftung statt der aus dem Katalog.
-- x, y eigene Lage in der Queransicht. Die Hochkant-Ansicht auf
-- schmalen Schirmen spiegelt sie selbst.
-- optionen JSON fuer Einstellungen, die nur ein Kreis kennt - etwa die
-- Aufteilung der PV-Leistung auf Flaechen.
--
-- Fehlt eine Zeile, gilt der Katalog. Ein neuer Kreis im Katalog erscheint
-- damit ohne Zutun, und eine leere Tabelle ist die Uebersicht dieses Hauses,
-- wie sie ausgeliefert wird.
-- ---------------------------------------------------------------------------
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `energiefluss` (
`id` varchar(20) NOT NULL COMMENT 'Kennung aus dem Katalog, z.B. og, wallboxEG',
`aktiv` tinyint(1) NOT NULL DEFAULT 1,
`name` varchar(40) DEFAULT NULL COMMENT 'NULL = Name aus dem Katalog',
`x` smallint(6) DEFAULT NULL COMMENT 'Mitte in der Queransicht; NULL = Katalog',
`y` smallint(6) DEFAULT NULL,
`optionen` text DEFAULT NULL COMMENT 'JSON, je Kreis verschieden; NULL = Katalog',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
+5 -2
View File
@@ -100,7 +100,8 @@ if (isset($_GET["addUser"])) {
*/ */
$pages = [ $pages = [
"solar" => ["template" => "solar.php", "mqtt" => true, "charts" => true, "meteogram" => true, "leaflet" => false, "solar" => ["template" => "solar.php", "mqtt" => true, "charts" => true, "meteogram" => true, "leaflet" => false,
"scripts" => ["js/solar/jahresstatistik.js", "js/solar/solarMQTT.js"]], "scripts" => ["js/solar/jahresstatistik.js", "js/solar/energieflussKatalog.js",
"js/solar/energiefluss.js", "js/solar/solarMQTT.js"]],
"home" => ["template" => "home.php", "mqtt" => true, "charts" => false, "meteogram" => false, "leaflet" => false, "home" => ["template" => "home.php", "mqtt" => true, "charts" => false, "meteogram" => false, "leaflet" => false,
"scripts" => ["js/solar/autoActionFuncs.js", "js/solar/zeitleiste.js", "js/solar/homeMQTT.js"]], "scripts" => ["js/solar/autoActionFuncs.js", "js/solar/zeitleiste.js", "js/solar/homeMQTT.js"]],
"heat" => ["template" => "heat.php", "mqtt" => true, "charts" => true, "meteogram" => false, "leaflet" => false, "heat" => ["template" => "heat.php", "mqtt" => true, "charts" => true, "meteogram" => false, "leaflet" => false,
@@ -112,7 +113,9 @@ if (isset($_GET["addUser"])) {
"weather" => ["template" => "weather.php", "mqtt" => true, "charts" => false, "meteogram" => true, "leaflet" => false, "weather" => ["template" => "weather.php", "mqtt" => true, "charts" => false, "meteogram" => true, "leaflet" => false,
"scripts" => ["js/solar/weatherMQTT.js"]], "scripts" => ["js/solar/weatherMQTT.js"]],
"settings" => ["template" => "settings.php", "mqtt" => false, "charts" => false, "meteogram" => false, "leaflet" => false, "settings" => ["template" => "settings.php", "mqtt" => false, "charts" => false, "meteogram" => false, "leaflet" => false,
"scripts" => ["js/solar/settings.js", "js/solar/grundriss.js"]], "scripts" => ["js/solar/settings.js", "js/solar/grundriss.js",
"js/solar/energieflussKatalog.js", "js/solar/energiefluss.js",
"js/solar/energieflussEinstellungen.js"]],
"logs" => ["template" => "logs.php", "mqtt" => false, "charts" => false, "meteogram" => false, "leaflet" => false, "logs" => ["template" => "logs.php", "mqtt" => false, "charts" => false, "meteogram" => false, "leaflet" => false,
"scripts" => ["js/solar/logs.js"]], "scripts" => ["js/solar/logs.js"]],
"einfuehrung" => ["template" => "einfuehrung.php", "mqtt" => false, "charts" => false, "meteogram" => false, "leaflet" => false, "einfuehrung" => ["template" => "einfuehrung.php", "mqtt" => false, "charts" => false, "meteogram" => false, "leaflet" => false,
+556
View File
@@ -0,0 +1,556 @@
/* ==========================================================================
Energiefluss-Übersicht: zeichnet den Katalog (energieflussKatalog.js) als
animiertes SVG und hält es mit den MQTT-Werten aktuell.
const bild = new Energiefluss(behaelter, {
einstellungen, // Abweichungen aus homeMesh.energiefluss
bearbeiten: false, // true = Einstellungsvorschau, Kreise ziehbar
onKlick(knoten), // Kreis mit klick-Ziel angetippt
onVerschoben(id, x, y), // nur beim Bearbeiten
onGezeichnet(), // nach jedem (Neu-)Zeichnen
});
bild.aktualisieren(mqttData);
Der Renderer kennt keinen Kreis beim Namen. Alles, was einen Kreis
ausmacht, steht im Katalog; hier steht nur, wie Kreise, Flüsse, Symbole
und Plaketten grundsätzlich aussehen.
Zwei Ansichten: quer (Lage aus dem Katalog) und hochkant für schmale
Schirme. Hochkant sind x und y vertauscht - aus "Quellen links, Verbraucher
rechts" wird "Quellen oben, Verbraucher unten". So braucht es keine zweite
Lage je Kreis, und eine eigene Lage aus den Einstellungen gilt in beiden.
Zur Leistung: die Flüsse laufen über stroke-dashoffset in groben Stufen
(steps), und alle Animationen stehen still, solange die Karte nicht zu
sehen ist. Werte werden nur geschrieben, wenn sie sich geändert haben.
========================================================================== */
(function () {
"use strict";
const NS = "http://www.w3.org/2000/svg";
const RADIUS = 78;
const RADIUS_GROSS = 92;
const RAND = 60; // Luft um die äußersten Kreise
const HOCHKANT_UNTER = 720; // px Breite, ab der hochkant gezeichnet wird
/* ------------------------------------------------------------------------
Symbole: 24er Raster, Mitte 0/0, Linien in currentColor. Teile mit
Klasse enf-s-* werden über CSS animiert oder über den Zustand gesetzt.
------------------------------------------------------------------------ */
const SYMBOLE = {
sonne: () => `
<g class="enf-s-strahlen">
${[0, 45, 90, 135, 180, 225, 270, 315].map(w =>
`<path d="M0-8.2V-11" transform="rotate(${w})"/>`).join("")}
</g>
<circle r="4.6" class="enf-s-kern"/>`,
netz: () => `
<path d="M-5 11.5L-1.6-10.5h3.2L5 11.5"/>
<path d="M-9.5-5.5h19M-7-1h14"/>
<path d="M-8.5-5.5v2.4M8.5-5.5v2.4M-6-1v2.4M6-1v2.4"/>
<path d="M-3.3 3.5l6.8 8M3.3 3.5l-6.8 8" class="enf-s-fein"/>`,
akku: () => `
<rect x="-6.5" y="-8.5" width="13" height="20" rx="2.4"/>
<path d="M-2.6-11h5.2"/>
<rect x="-4" y="-6" width="8" height="15" rx="1" class="enf-s-stand"/>
<path d="M1.2-4.2l-4 5.6h3l-1.4 4.6 4.2-6h-3z" class="enf-s-blitz"/>`,
wechselrichter: () => `
<rect x="-10.5" y="-10.5" width="21" height="21" rx="3.5"/>
<path d="M-10.5 10.5l21-21" class="enf-s-fein"/>
<path d="M-7.5-5.2h5M-7.5-2.4h5"/>
<path d="M1.8 4.2c1-1.7 2.1-1.7 3.1 0s2.1 1.7 3.1 0" class="enf-s-welle"/>`,
verbrauch: () => `
<path d="M-11-1.5L0-10.5l11 9"/>
<path d="M-8-4v14.5h16V-4"/>
<path d="M1.3-3.8l-4.4 6.2h3.3l-1.6 5 4.6-6.6H-.1z" class="enf-s-blitz enf-s-an"/>`,
etageOben: () => etage(-4.5, 1.5),
etageMitte: () => etage(1.5, 7.5),
etageUnten: () => etage(7.5, 11.5),
wallbox: () => `
<rect x="-10.5" y="-10.5" width="11" height="21" rx="2.2"/>
<rect x="-8" y="-7.5" width="6" height="4" rx=".8" class="enf-s-fein"/>
<path d="M-6.4 1.2l-2 3.5h2.2l-1 3.2 2.9-4.2h-2.2z" class="enf-s-blitz"/>
<path d="M.5 2h2.8a2.3 2.3 0 0 1 2.3 2.3v3.9a1.9 1.9 0 0 0 3.8 0V-.5" class="enf-s-kabel"/>
<path d="M7.2-4.6h3.6v2.3a1.8 1.8 0 0 1-3.6 0z"/>
<path d="M8.2-4.6v-2M9.8-4.6v-2"/>`,
speicher: () => `
<defs><clipPath id="{id}-tank"><rect x="-6.8" y="-10.8" width="13.6" height="19.6" rx="5.2"/></clipPath></defs>
<g clip-path="url(#{id}-tank)" class="enf-s-temps">
<rect x="-8" y="-12" width="16" height="7.6" class="enf-s-t0"/>
<rect x="-8" y="-4.4" width="16" height="6.6" class="enf-s-t1"/>
<rect x="-8" y="2.2" width="16" height="7.6" class="enf-s-t2"/>
</g>
<rect x="-7" y="-11" width="14" height="20" rx="5.4"/>
<path d="M-3.4 5.6l1.1-1.8 1.2 1.8 1.1-1.8 1.2 1.8 1.1-1.8" class="enf-s-spirale"/>
<path d="M-4 9v2.5M4 9v2.5"/>`,
tropfen: () => `
<path d="M0-11c4.6 5.5 7 9 7 12.6a7 7 0 0 1-14 0C-7-2-4.6-5.5 0-11z"/>
<path d="M-3.6 3.4c1.2 1 2.4 1 3.6 0s2.4-1 3.6 0" class="enf-s-welle"/>`,
};
function etage(von, bis) {
return `
<rect x="-8" y="${von}" width="16" height="${bis - von}" class="enf-s-ebene"/>
<path d="M-11.5-3L0-11l11.5 8"/>
<path d="M-8-5v12.5h16V-5"/>
<path d="M-8 1.5h16" class="enf-s-fein"/>
<path d="M-12 7.5h24"/>
<path d="M-8 7.5v4h16v-4" class="enf-s-fein enf-s-keller"/>`;
}
/* Plaketten am Kreis: Zeichen aus Bootstrap Icons (liegt ohnehin im Dashboard). */
const ZEICHEN = {
eco: { glyph: "奈", farbe: "#6cbe58" }, // bi-leaf
uhr: { glyph: "", farbe: "#e8a33a" }, // bi-clock-fill
manuell: { glyph: "", farbe: "#c9a227" }, // bi-sliders
stecker: { glyph: "", farbe: "#8fb8de" }, // bi-plug-fill
schloss: { glyph: "", farbe: "#b9c0c8" }, // bi-lock-fill
heizung: { glyph: "", farbe: "#e5624f" }, // bi-fire
tank: { glyph: "", farbe: "#b9c0c8" }, // bi-fuel-pump-fill
warnung: { glyph: "", farbe: "#e5624f" }, // bi-exclamation-triangle-fill
};
/* ------------------------------------------------------------------------
Hilfen
------------------------------------------------------------------------ */
function el(name, attr, eltern) {
const e = document.createElementNS(NS, name);
if (attr) for (const k in attr) e.setAttribute(k, attr[k]);
if (eltern) eltern.appendChild(e);
return e;
}
function leistungText(w) {
const a = Math.abs(w);
if (a < 1000) return Math.round(a) + " W";
return (a / 1000).toFixed(a < 10000 ? 2 : 1).replace(".", ",") + " kW";
}
/** Liest mqttData: Werte kommen als Buffer, Zahl und Text wie gehabt. */
function leser(daten) {
const cache = {};
return {
text(topic) {
const v = daten[topic];
return v === undefined ? undefined : String(v);
},
zahl(topic) {
const v = Number(String(daten[topic]));
return isFinite(v) ? v : 0;
},
json(topic) {
if (topic in cache) return cache[topic];
try { cache[topic] = JSON.parse(String(daten[topic])); } catch (e) { cache[topic] = null; }
return cache[topic];
},
};
}
/** Farbe zwischen kalt und heiß, wie früher assignColor() fürs Pufferbild. */
function tempFarbe(t) {
const f = Math.max(0, Math.min(1, (t - 20) / 60));
const kalt = [35, 137, 186], heiss = [186, 59, 35];
return "rgb(" + kalt.map((k, i) => Math.round(k + (heiss[i] - k) * f)).join(",") + ")";
}
let laufendeNummer = 0;
/* ------------------------------------------------------------------------
Die Übersicht
------------------------------------------------------------------------ */
class Energiefluss {
constructor(behaelter, opt = {}) {
this.behaelter = behaelter;
this.opt = opt;
this.bearbeiten = !!opt.bearbeiten;
this.nummer = ++laufendeNummer;
this.letzte = null;
this.setzeEinstellungen(opt.einstellungen || {});
// Hochkant oder quer - neu zeichnen nur, wenn die Ansicht wechselt.
this.beobachter = new ResizeObserver(() => {
const hoch = this.hochkantGewuenscht();
if (hoch !== this.hochkant) this.zeichnen();
});
this.beobachter.observe(behaelter);
// Animationen anhalten, solange niemand hinsieht.
this.sichtbar = true;
this.sicht = new IntersectionObserver(eintraege => {
this.sichtbar = eintraege[0].isIntersecting;
this.pausieren();
});
this.sicht.observe(behaelter);
document.addEventListener("visibilitychange", () => this.pausieren());
}
/** Katalog und Abweichungen zu einer Liste zusammenführen. */
setzeEinstellungen(einstellungen) {
this.einstellungen = einstellungen;
this.knoten = ENERGIEFLUSS_KATALOG.knoten.map(k => {
const e = einstellungen[k.id] || {};
return Object.assign({}, k, {
aktiv: k.pflicht || e.aktiv !== false,
anzeigeName: e.name || k.name,
lage: [e.x ?? k.x, e.y ?? k.y],
optionenWirksam: Object.assign({}, k.optionen || {}, e.optionen || {}),
});
});
this.nachId = Object.fromEntries(this.knoten.map(k => [k.id, k]));
// Wirksamer Elternkreis: der nächste eingeschaltete in der Kette.
for (const k of this.knoten) {
let e = k.eltern ? this.nachId[k.eltern] : null;
while (e && !e.aktiv) e = e.eltern ? this.nachId[e.eltern] : null;
k.elternWirksam = e;
let tiefe = 0;
for (let p = e; p; p = p.elternWirksam) tiefe++;
k.tiefe = tiefe;
}
}
hochkantGewuenscht() {
return !this.bearbeiten && this.behaelter.clientWidth > 0 && this.behaelter.clientWidth < HOCHKANT_UNTER;
}
/** Lage eines Kreises in der aktuellen Ansicht. */
punkt(k) {
return this.hochkant ? [k.lage[1], k.lage[0]] : [k.lage[0], k.lage[1]];
}
radius(k) {
return k.gross ? RADIUS_GROSS : RADIUS;
}
sichtbareKnoten() {
return this.knoten.filter(k => k.aktiv || this.bearbeiten);
}
/* ---------------------------------------------------------- Zeichnen */
zeichnen() {
this.hochkant = this.hochkantGewuenscht();
this.behaelter.innerHTML = "";
const svg = this.svg = el("svg", {
class: "enf" + (this.bearbeiten ? " enf-bearbeiten" : "") + (this.hochkant ? " enf-hoch" : ""),
role: "img", "aria-label": "Energiefluss",
}, this.behaelter);
this.ebeneFluss = el("g", { class: "enf-fluesse" }, svg);
this.ebeneKnoten = el("g", { class: "enf-knotenebene" }, svg);
this.elemente = {};
for (const k of this.sichtbareKnoten()) this.knotenZeichnen(k);
for (const k of this.sichtbareKnoten()) if (k.aktiv && k.elternWirksam) this.flussZeichnen(k);
this.rahmenSetzen();
this.pausieren();
if (this.letzte) {
const d = this.letzte;
this.letzte = null;
this.aktualisieren(d);
}
if (this.opt.onGezeichnet) this.opt.onGezeichnet();
}
rahmenSetzen() {
const ks = this.sichtbareKnoten();
if (!ks.length) return;
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
for (const k of ks) {
const [x, y] = this.punkt(k), r = this.radius(k);
x0 = Math.min(x0, x - r - 20); x1 = Math.max(x1, x + r + 20);
y0 = Math.min(y0, y - r - 20); y1 = Math.max(y1, y + r + 56); // Name unter dem Kreis
}
this.svg.setAttribute("viewBox",
[x0 - RAND / 2, y0 - RAND / 2, x1 - x0 + RAND, y1 - y0 + RAND].map(Math.round).join(" "));
}
knotenZeichnen(k) {
const [x, y] = this.punkt(k);
const r = this.radius(k);
const id = "enf" + this.nummer + "-" + k.id;
const g = el("g", {
class: "enf-knoten" + (k.aktiv ? "" : " enf-aus") + ((k.klick && !this.bearbeiten) ? " enf-klickbar" : ""),
transform: `translate(${x} ${y})`,
style: `--farbe:${k.farbe};--verz:${k.tiefe * 110}ms`,
"data-knoten": k.id,
}, this.ebeneKnoten);
const koerper = el("g", { class: "enf-koerper" }, g);
el("circle", { r: r + 17, class: "enf-hof" }, koerper);
el("circle", { r, class: "enf-spur" }, koerper);
const ring = el("g", { class: "enf-ring", transform: "rotate(-90)" }, koerper);
el("circle", { r: r - 11, class: "enf-scheibe" }, koerper);
// Füllstand (Batterie, Auto): Welle, die mit dem Stand steigt.
let fuellung = null;
if (k.symbol === "akku" || k.symbol === "wallbox") {
el("clipPath", { id: id + "-innen" }, el("defs", null, koerper))
.appendChild(el("circle", { r: r - 11 }));
const clip = el("g", { "clip-path": `url(#${id}-innen)` }, koerper);
fuellung = el("g", { class: "enf-fuellung", style: `--stand:0;--r:${r - 11}` }, clip);
const b = r * 4;
el("path", {
class: "enf-welle",
d: `M${-b} 0 q${r / 2} -9 ${r} 0 t${r} 0 t${r} 0 t${r} 0 t${r} 0 t${r} 0 t${r} 0 t${r} 0 V${r * 2} H${-b} Z`,
}, fuellung);
}
const symbol = el("g", {
class: "enf-symbol enf-sym-" + k.symbol,
transform: `translate(0 ${-r * 0.43}) scale(${k.gross ? 1.95 : 1.75})`,
}, koerper);
symbol.innerHTML = SYMBOLE[k.symbol]().replace(/\{id\}/g, id);
const wert = el("text", { class: "enf-wert", y: r * 0.2 + 6 }, koerper);
const neben = el("text", { class: "enf-neben", y: r * 0.2 + 33 }, koerper);
const name = el("text", { class: "enf-name", y: r + 46 }, g);
name.textContent = k.anzeigeName;
const zeichen = el("g", { class: "enf-zeichen" }, koerper);
if (k.klick && !this.bearbeiten) {
const t = el("g", { class: "enf-tipp", transform: `translate(${r * 0.72} ${-r * 0.72})` }, koerper);
el("circle", { r: 15 }, t);
el("path", { d: "M-3.5-6l6 6-6 6" }, t);
g.setAttribute("tabindex", "0");
g.setAttribute("role", "button");
g.setAttribute("aria-label", k.anzeigeName + " Details");
const los = () => this.opt.onKlick && this.opt.onKlick(k);
g.addEventListener("click", los);
g.addEventListener("keydown", e => {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); los(); }
});
}
// Bewässerung: Zonen als Zeilen im Kreis. Die Ids gehören solarMQTT.js
// und werden nur außerhalb der Einstellungsvorschau vergeben.
if (k.zonen) {
wert.remove(); neben.remove();
symbol.setAttribute("transform", `translate(0 ${-r * 0.5}) scale(1.4)`);
k.zonen.forEach((z, i) => {
const zy = -r * 0.1 + i * 25;
const glyph = el("text", { class: "enf-zone-glyph", x: -r * 0.52, y: zy }, koerper);
glyph.textContent = z.zeichen;
el("title", null, glyph).textContent = z.name;
const nm = el("text", { class: "enf-zone-name", x: -r * 0.36, y: zy }, koerper);
nm.textContent = z.name;
const st = el("text", { class: "enf-zone-status", x: r * 0.6, y: zy }, koerper);
st.textContent = "Aus";
if (!this.bearbeiten) { glyph.id = z.symbol; st.id = z.status; }
});
}
if (this.bearbeiten) this.ziehbarMachen(g, k);
this.elemente[k.id] = { g, ring, wert, neben, zeichen, symbol, fuellung, fluss: null, segmente: [] };
}
flussPfad(k) {
const e = k.elternWirksam;
const a = this.punkt(e), b = this.punkt(k);
// Knick: quer vom Elternkreis senkrecht los, dann waagerecht in den Kreis
// (Ecke x vom Eltern, y vom Kind); hochkant genau umgekehrt.
let c;
if (Math.abs(a[0] - b[0]) < 30 || Math.abs(a[1] - b[1]) < 30) {
c = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
} else {
c = this.hochkant ? [b[0], a[1]] : [a[0], b[1]];
}
const rand = (p, q, r) => {
const dx = q[0] - p[0], dy = q[1] - p[1], l = Math.hypot(dx, dy) || 1;
return [p[0] + dx / l * (r + 20), p[1] + dy / l * (r + 20)];
};
const s = rand(a, c, this.radius(e)), z = rand(b, c, this.radius(k));
return `M${s[0].toFixed(1)} ${s[1].toFixed(1)} Q${c[0]} ${c[1]} ${z[0].toFixed(1)} ${z[1].toFixed(1)}`;
}
flussZeichnen(k) {
const d = this.flussPfad(k);
const g = el("g", { class: "enf-fluss enf-still", style: `--farbe:${k.farbe};--verz:${k.tiefe * 110 + 150}ms` }, this.ebeneFluss);
const bett = el("path", { d, class: "enf-bett", pathLength: "1" }, g);
const hof = el("path", { d, class: "enf-strom-hof" }, g);
const strom = el("path", { d, class: "enf-strom" }, g);
this.elemente[k.id].fluss = { g, bett, hof, strom };
}
/* ------------------------------------------------------ Aktualisieren */
aktualisieren(daten) {
this.letzte = daten;
if (!this.svg) return;
const m = leser(daten);
for (const k of this.sichtbareKnoten()) {
const e = this.elemente[k.id];
if (!e) continue;
let z;
try {
z = k.lesen(m, k.optionenWirksam) || {};
} catch (fehler) {
console.warn("Energiefluss: " + k.id, fehler);
continue;
}
this.knotenSetzen(k, e, z);
if (e.fluss) this.flussSetzen(e.fluss, z.fluss || 0);
}
}
knotenSetzen(k, e, z) {
// Werte
const wert = z.wert !== undefined ? z.wert : leistungText(z.leistung || 0);
if (e.wert.isConnected && e.wert.textContent !== wert) e.wert.textContent = wert;
const neben = z.neben || "";
if (e.neben.isConnected && e.neben.textContent !== neben) e.neben.textContent = neben;
e.g.classList.toggle("enf-ruhe", !!z.ruhe && k.aktiv);
// Ring: Segmente in Hundertsteln des Umfangs
let segmente = z.segmente || (z.leistung !== undefined ? [{ w: Math.abs(z.leistung), farbe: k.farbe }] : []);
segmente = this.segmenteZusammenfassen(segmente);
const skala = k.skala || 15000;
while (e.segmente.length < segmente.length) {
e.segmente.push(el("circle", { r: this.radius(k), class: "enf-bogen", pathLength: "100" }, e.ring));
}
let start = 0;
e.segmente.forEach((kreis, i) => {
const s = segmente[i];
let laenge = s ? Math.max(0, Math.min(100 - start, (s.w / skala) * 100)) : 0;
if (laenge < 0.4) laenge = 0;
const luecke = laenge > 0 && start > 0 ? 0.6 : 0;
const a = (laenge - luecke).toFixed(2), off = (-(start + luecke)).toFixed(2);
const schluessel = a + "|" + off + "|" + (s ? s.farbe : "");
if (kreis._stand !== schluessel) {
kreis._stand = schluessel;
kreis.style.strokeDasharray = `${Math.max(0, a)} 100`;
kreis.style.strokeDashoffset = off;
if (s) kreis.style.stroke = s.farbe;
}
start += laenge;
});
// Zustand des Symbols
const zu = z.zustand || {};
e.g.classList.toggle("enf-an", !!zu.an);
e.g.classList.toggle("enf-laden", !!zu.laden);
if (zu.stand !== undefined && isFinite(zu.stand)) {
const s = Math.max(0, Math.min(1, zu.stand)).toFixed(3);
if (e.g.style.getPropertyValue("--stand") !== s) {
e.g.style.setProperty("--stand", s);
if (e.fuellung) e.fuellung.style.setProperty("--stand", s);
}
}
if (zu.temps) {
zu.temps.forEach((t, i) => {
const r = e.symbol.querySelector(".enf-s-t" + i);
const f = tempFarbe(t);
if (r && r._f !== f) { r._f = f; r.style.fill = f; }
});
}
// Plaketten
const zeichen = z.zeichen || [];
const sig = zeichen.map(x => x.art + ":" + (x.text || "") + ":" + (x.titel || "")).join("|");
if (e.zeichen._sig !== sig) {
e.zeichen._sig = sig;
this.zeichenSetzen(k, e.zeichen, zeichen);
}
}
/** Anteile ausgeschalteter Kreise fallen ins letzte Segment. */
segmenteZusammenfassen(segmente) {
const aus = segmente.filter(s => s.knoten && this.nachId[s.knoten] && !this.nachId[s.knoten].aktiv);
if (!aus.length) return segmente;
const rest = segmente.filter(s => !aus.includes(s)).map(s => Object.assign({}, s));
if (rest.length) rest[rest.length - 1].w += aus.reduce((a, s) => a + Math.max(0, s.w), 0);
return rest;
}
zeichenSetzen(k, gruppe, zeichen) {
gruppe.innerHTML = "";
const r = this.radius(k);
zeichen.forEach((z, i) => {
const art = ZEICHEN[z.art];
if (!art) return;
// Außen am Ring rechts unten, eine Plakette je 36 Grad - so bleiben
// Wert und Zeile darunter frei.
const winkel = (88 + i * 36) * Math.PI / 180;
const px = Math.sin(winkel) * (r + 12), py = -Math.cos(winkel) * (r + 12);
const p = el("g", { class: "enf-plakette", transform: `translate(${px.toFixed(1)} ${py.toFixed(1)})`, style: `--pfarbe:${art.farbe};--i:${i}` }, gruppe);
if (z.titel) el("title", null, p).textContent = z.titel;
if (z.text) {
const breite = 26 + z.text.length * 10;
el("rect", { x: -16, y: -16, width: breite, height: 32, rx: 16 }, p);
el("text", { class: "enf-plakette-text", x: 12, y: 1 }, p).textContent = z.text;
} else {
el("circle", { r: 16 }, p);
}
el("text", { class: "enf-plakette-glyph", x: 0, y: 1 }, p).textContent = art.glyph;
});
}
flussSetzen(f, w) {
const a = Math.abs(w);
const still = a < 15;
const breite = still ? 0 : 3.5 + 6.5 * (1 - Math.exp(-a / 3000));
// Schneller bei mehr Leistung: 2,6 s je Abschnitt bei wenig, 0,7 s bei viel.
const dauer = still ? 0 : 0.7 + 1.9 * Math.exp(-a / 3000);
const sig = still ? "still" : breite.toFixed(1) + "|" + dauer.toFixed(2) + "|" + (w < 0);
if (f._sig === sig) return;
f._sig = sig;
f.g.classList.toggle("enf-still", still);
f.g.classList.toggle("enf-rueck", w < 0);
f.g.style.setProperty("--breite", breite.toFixed(1));
f.g.style.setProperty("--dauer", dauer.toFixed(2) + "s");
}
pausieren() {
if (!this.svg) return;
this.svg.classList.toggle("enf-pause", !this.sichtbar || document.hidden);
}
/* -------------------------------------------------------- Bearbeiten */
ziehbarMachen(g, k) {
g.classList.add("enf-ziehbar");
g.addEventListener("pointerdown", ev => {
ev.preventDefault();
g.setPointerCapture(ev.pointerId);
const pt = this.svg.createSVGPoint();
const svgPunkt = e2 => {
pt.x = e2.clientX; pt.y = e2.clientY;
return pt.matrixTransform(this.svg.getScreenCTM().inverse());
};
const start = svgPunkt(ev);
const [x0, y0] = k.lage;
g.classList.add("enf-zieht");
const bewegen = e2 => {
const p = svgPunkt(e2);
k.lage = [Math.round((x0 + p.x - start.x) / 10) * 10, Math.round((y0 + p.y - start.y) / 10) * 10];
g.setAttribute("transform", `translate(${k.lage[0]} ${k.lage[1]})`);
for (const kind of this.knoten) {
const f = this.elemente[kind.id] && this.elemente[kind.id].fluss;
if (f && (kind === k || kind.elternWirksam === k)) {
const d = this.flussPfad(kind);
f.bett.setAttribute("d", d); f.hof.setAttribute("d", d); f.strom.setAttribute("d", d);
}
}
};
const ende = () => {
g.removeEventListener("pointermove", bewegen);
g.classList.remove("enf-zieht");
this.rahmenSetzen();
if (this.opt.onVerschoben) this.opt.onVerschoben(k.id, k.lage[0], k.lage[1]);
};
g.addEventListener("pointermove", bewegen);
g.addEventListener("pointerup", ende, { once: true });
g.addEventListener("pointercancel", ende, { once: true });
});
}
}
window.Energiefluss = Energiefluss;
})();
+279
View File
@@ -0,0 +1,279 @@
/* ==========================================================================
Reiter "Übersicht": die Energiefluss-Übersicht an dieses Haus anpassen.
Links die Übersicht selbst im Bearbeitungsmodus (energiefluss.js) mit
Beispielwerten, rechts je Kreis Schalter und Name, beim PV-Kreis die
Flächen. Verschoben wird in der Vorschau. Gespeichert wird der ganze Stand
auf einmal (restricted/energiefluss.php hält davon nur die Abweichungen).
Nutzt textSchuetzen() aus settings.js.
========================================================================== */
(function () {
"use strict";
// Beispielwerte: ein sonniger Nachmittag, Auto lädt im EG, Heizstab läuft.
const BEISPIEL = {
"solarManager/P_PV": "8400", "solarManager/P_PVn": JSON.stringify([2600, 1300, 130, 130, 140, 140, 130, 130, 135, 135, 130, 130, 120, 120,
155, 150, 152, 158, 150, 149, 150, 154, 151, 150, 153, 155, 150, 92, 150, 152, 151, 150, 149, 0]),
"solarManager/P_Grid": "-900", "solarManager/i_l1evu": "-1.3", "solarManager/i_l2evu": "-1.4", "solarManager/i_l3evu": "-1.2",
"solarManager/P_Akku": "-2100", "solarManager/SOC": "64", "solarManager/crgMaxPct": "100",
"solarManager/autarky": "100", "solarManager/eff": "96",
"solarManager/P_Load": "-5400", "solarManager/og": "-350", "solarManager/eg": "3720", "solarManager/ug": "270",
"solarManager/pHeat": "1000", "solarManager/t_buffT": "61", "solarManager/t_buffM": "48", "solarManager/t_buffB": "35",
"solarManager/heatMode": "eco", "solarManager/heatOG": "1", "solarManager/heatEG": "0",
"solarManager/evPower": "3.2", "solarManager/evSOC": "78", "solarManager/evFuel": "0", "solarManager/evMode": "Eco",
"solarManager/evPlug": "1", "solarManager/evLock": "1", "solarManager/carRemChrg": "95",
"solarManager/evPowerOG": "0", "solarManager/evPlugOG": "0", "solarManager/evModeOG": "Eco",
};
let bild = null;
let gespeichert = {}; // Stand der Datenbank
let stand = {}; // Stand der Maske
let geladen = false;
const $ = id => document.getElementById(id);
const katalog = () => ENERGIEFLUSS_KATALOG.knoten;
const kopie = o => JSON.parse(JSON.stringify(o));
function hinweis(text, klasse) {
const ziel = $("energieflussHinweis");
if (!ziel) return;
ziel.className = "small " + (klasse || "text-body-secondary");
ziel.textContent = text;
}
function geaendert() {
const sauber = s => JSON.stringify(Object.keys(s).sort().map(k => [k, s[k]]));
const anders = sauber(normalisiert(stand)) !== sauber(normalisiert(gespeichert));
hinweis(anders ? "Nicht gespeichert." : "", anders ? "text-warning" : "");
}
/** Nur echte Abweichungen, in fester Form - zum Vergleichen und Schicken. */
function normalisiert(s) {
const aus = {};
for (const k of katalog()) {
const e = s[k.id] || {};
const eintrag = {
aktiv: k.pflicht ? true : e.aktiv !== false,
name: (e.name || "").trim() || null,
x: e.x ?? null, y: e.y ?? null,
optionen: e.optionen || null,
};
if (eintrag.x === k.x && eintrag.y === k.y) { eintrag.x = null; eintrag.y = null; }
if (!eintrag.aktiv || eintrag.name || eintrag.x !== null || eintrag.optionen) aus[k.id] = eintrag;
}
return aus;
}
function vorschauZeichnen() {
if (!bild) {
bild = new Energiefluss($("energieflussVorschau"), {
bearbeiten: true,
einstellungen: stand,
onVerschoben(id, x, y) {
stand[id] = Object.assign({}, stand[id], { x, y });
listeZeichnen();
geaendert();
},
});
}
bild.setzeEinstellungen(normalisiert(stand));
bild.zeichnen();
bild.aktualisieren(BEISPIEL);
}
/* ------------------------------------------------------------ Liste */
function listeZeichnen() {
const ziel = $("energieflussListe");
const wirksam = bild ? bild.nachId : {};
ziel.innerHTML = katalog().map(k => {
const e = stand[k.id] || {};
const aktiv = k.pflicht || e.aktiv !== false;
const w = wirksam[k.id];
const eltern = w && w.elternWirksam;
const elternName = eltern ? (stand[eltern.id] && stand[eltern.id].name) || eltern.titel || eltern.name : null;
const verschoben = e.x != null && (e.x !== k.x || e.y !== k.y);
const flaechen = k.id === "pv" && aktiv ? flaechenZeichnen(k, e) : "";
return `
<div class="enf-e-zeile${aktiv ? "" : " enf-e-aus"}" data-id="${k.id}">
<div class="form-check form-switch m-0" title="${k.pflicht ? "Gehört immer dazu" : "Ein- oder ausschalten"}">
<input class="form-check-input" type="checkbox" id="enf-aktiv-${k.id}" data-feld="aktiv"
${aktiv ? "checked" : ""} ${k.pflicht ? "disabled" : ""}>
</div>
<label class="enf-e-name" for="enf-name-${k.id}">
<span class="enf-e-punkt" style="--farbe:${k.farbe}"></span>
<input class="form-control form-control-sm" id="enf-name-${k.id}" data-feld="name" maxlength="40"
placeholder="${textSchuetzen(k.name)}" title="${textSchuetzen(k.titel || k.name)}" value="${textSchuetzen(e.name || "")}" ${aktiv ? "" : "disabled"}>
</label>
<span class="enf-e-eltern small text-body-secondary">${elternName ? "an " + textSchuetzen(elternName) : k.eltern ? "" : "frei"}</span>
<button type="button" class="btn btn-sm btn-link text-body-secondary p-0" data-aktion="lage"
title="Lage aus dem Katalog" ${verschoben ? "" : "hidden"}><i class="bi bi-arrow-counterclockwise"></i></button>
${flaechen}
</div>`;
}).join("");
}
function flaechenZeichnen(k, e) {
const liste = (e.optionen && e.optionen.flaechen) || k.optionen.flaechen;
const eigen = !!(e.optionen && e.optionen.flaechen);
return `
<div class="enf-e-flaechen">
<div class="small text-body-secondary mb-1">
Flächen im Sonnenring welche Einträge von <code>P_PVn</code> dazugehören
${eigen ? `<button type="button" class="btn btn-sm btn-link p-0 ms-2" data-aktion="flaechen-zurueck">Katalog</button>` : ""}
</div>
${liste.map((f, i) => `
<div class="enf-e-flaeche" data-index="${i}">
<input type="color" class="form-control form-control-sm form-control-color" data-flaeche="farbe" value="${f.farbe}" title="Farbe">
<input class="form-control form-control-sm" data-flaeche="name" value="${textSchuetzen(f.name)}" placeholder="Name" maxlength="20">
<input class="form-control form-control-sm" type="number" min="0" max="99" data-flaeche="von" value="${f.von}" title="erster Eintrag">
<input class="form-control form-control-sm" type="number" min="0" max="99" data-flaeche="bis" value="${f.bis}" title="letzter Eintrag">
<button type="button" class="btn btn-sm btn-link text-body-secondary p-0" data-aktion="flaeche-weg" title="Fläche entfernen"><i class="bi bi-x-lg"></i></button>
</div>`).join("")}
<button type="button" class="btn btn-sm btn-outline-secondary mt-1" data-aktion="flaeche-neu"><i class="bi bi-plus-lg me-1"></i>Fläche</button>
</div>`;
}
function flaechenLesen(zeile) {
return [...zeile.querySelectorAll(".enf-e-flaeche")].map(r => ({
name: r.querySelector('[data-flaeche="name"]').value.trim(),
von: Math.max(0, parseInt(r.querySelector('[data-flaeche="von"]').value, 10) || 0),
bis: Math.max(0, parseInt(r.querySelector('[data-flaeche="bis"]').value, 10) || 0),
farbe: r.querySelector('[data-flaeche="farbe"]').value,
}));
}
function flaechenSetzen(id, flaechen) {
const e = stand[id] = Object.assign({}, stand[id]);
e.optionen = Object.assign({}, e.optionen, { flaechen });
}
/* --------------------------------------------------------- Ereignisse */
function binden() {
const liste = $("energieflussListe");
liste.addEventListener("change", ev => {
const zeile = ev.target.closest(".enf-e-zeile");
if (!zeile) return;
const id = zeile.dataset.id;
if (ev.target.dataset.feld === "aktiv") {
stand[id] = Object.assign({}, stand[id], { aktiv: ev.target.checked });
vorschauZeichnen();
listeZeichnen();
} else if (ev.target.dataset.flaeche) {
flaechenSetzen(id, flaechenLesen(zeile));
vorschauZeichnen();
}
geaendert();
});
liste.addEventListener("input", ev => {
const zeile = ev.target.closest(".enf-e-zeile");
if (!zeile || ev.target.dataset.feld !== "name") return;
stand[zeile.dataset.id] = Object.assign({}, stand[zeile.dataset.id], { name: ev.target.value });
clearTimeout(binden._uhr);
binden._uhr = setTimeout(vorschauZeichnen, 300);
geaendert();
});
liste.addEventListener("click", ev => {
const knopf = ev.target.closest("[data-aktion]");
if (!knopf) return;
const zeile = knopf.closest(".enf-e-zeile");
const id = zeile.dataset.id;
switch (knopf.dataset.aktion) {
case "lage":
stand[id] = Object.assign({}, stand[id], { x: null, y: null });
break;
case "flaeche-neu": {
const f = flaechenLesen(zeile);
const bis = f.length ? f[f.length - 1].bis + 1 : 0;
f.push({ name: "Fläche " + (f.length + 1), von: bis, bis, farbe: "#f7c000" });
flaechenSetzen(id, f);
break;
}
case "flaeche-weg": {
const f = flaechenLesen(zeile);
f.splice(Number(knopf.closest(".enf-e-flaeche").dataset.index), 1);
flaechenSetzen(id, f);
break;
}
case "flaechen-zurueck":
if (stand[id] && stand[id].optionen) {
const o = Object.assign({}, stand[id].optionen);
delete o.flaechen;
stand[id].optionen = Object.keys(o).length ? o : null;
}
break;
default:
return;
}
vorschauZeichnen();
listeZeichnen();
geaendert();
});
$("energieflussSpeichern").addEventListener("click", speichern);
$("energieflussNeuLaden").addEventListener("click", () => {
stand = kopie(gespeichert);
vorschauZeichnen(); listeZeichnen(); geaendert();
});
$("energieflussZuruecksetzen").addEventListener("click", () => {
stand = {};
vorschauZeichnen(); listeZeichnen(); geaendert();
});
}
async function speichern() {
const knopf = $("energieflussSpeichern");
knopf.disabled = true;
try {
const n = normalisiert(stand);
const knoten = Object.keys(n).map(id => Object.assign({ id }, n[id]));
const antwort = await fetch("ajax/settings.php?action=energiefluss-save", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ knoten }),
});
const d = await antwort.json();
if (!antwort.ok || d.error) throw new Error(d.error || "HTTP " + antwort.status);
gespeichert = d.einstellungen || {};
stand = kopie(gespeichert);
vorschauZeichnen(); listeZeichnen();
hinweis("Gespeichert die Solar-Seite zeigt es beim nächsten Laden.", "text-success");
} catch (e) {
hinweis(e.message, "text-danger");
} finally {
knopf.disabled = false;
}
}
async function laden() {
if (geladen) return;
geladen = true;
try {
const antwort = await fetch("ajax/settings.php?action=energiefluss", { cache: "no-store" });
const d = await antwort.json();
if (!antwort.ok || d.error) throw new Error(d.error || "HTTP " + antwort.status);
gespeichert = d.einstellungen || {};
stand = kopie(gespeichert);
binden();
vorschauZeichnen();
listeZeichnen();
} catch (e) {
geladen = false;
$("energieflussListe").innerHTML = '<div class="alert alert-danger mb-0">Übersicht nicht abrufbar: '
+ textSchuetzen(e.message) + "</div>";
}
}
document.addEventListener("DOMContentLoaded", () => {
if (!$("energieflussVorschau") || typeof Energiefluss === "undefined") return;
// Erst zeichnen, wenn der Reiter offen ist - verborgen hat die Vorschau keine Größe.
const knopf = $("einstellung-energiefluss-tab");
if (knopf) knopf.addEventListener("shown.bs.tab", laden);
if ($("einstellung-energiefluss").classList.contains("active")) laden();
// Ein Anker in der Adresse öffnet den Reiter erst nach DOMContentLoaded.
setTimeout(() => { if ($("einstellung-energiefluss").classList.contains("active")) laden(); }, 0);
});
})();
+290
View File
@@ -0,0 +1,290 @@
/* ==========================================================================
Katalog der Energiefluss-Übersicht (Solar-Seite, Karte "Realtime").
Jeder Kreis steht hier genau einmal: wie er heißt, wie er aussieht, woran
er hängt, wo er steht und woher seine Werte kommen. Gezeichnet wird er von
js/solar/energiefluss.js, ohne dass der Renderer einen Kreis beim Namen
kennt. Ein neuer Kreis ist also ein neuer Eintrag hier - sonst nichts.
Was ein Haus davon abweichend einstellt (Kreis aus, eigener Name, eigene
Lage, Optionen), steht in homeMesh.energiefluss und wird unter
Einstellungen -> Übersicht gepflegt.
Felder eines Eintrags:
id feste Kennung, steht in der Datenbank
name Beschriftung unter dem Kreis
titel Bezeichnung in den Einstellungen, falls name dort missverständlich ist
symbol Schlüssel in ENERGIEFLUSS_SYMBOLE (energiefluss.js)
farbe Hauptfarbe: Ring, Symbol, Fluss
eltern Kennung des Kreises, mit dem er über einen Fluss verbunden
ist; null = frei stehend. Ist der Elternkreis aus, hängt der
Kreis am nächsten eingeschalteten darüber.
pflicht lässt sich nicht ausschalten (die Mitte des Bildes)
gross größerer Kreis
x, y Mitte in der Queransicht (Zeichenfläche etwa 1400 x 850)
skala Leistung in W, bei der der Ring voll ist
klick {ansicht: "anlage"} oder {modal: "heater"} - Ziel beim Antippen
optionen Vorgabe für die Optionen, die in den Einstellungen änderbar sind
zonen nur Bewässerung: die Anzeigen, die solarMQTT.js beschreibt
lesen(m, opt) liefert den Zustand aus den MQTT-Werten:
leistung W, bestimmt den Hauptwert und den Ring
wert Text statt der Leistung (optional)
neben kleine Zeile unter dem Wert
segmente [{w, farbe, knoten?}] statt eines einzigen Rings; knoten =
der Anteil gehört zu diesem Kreis und fällt, ist der aus, in
das letzte Segment
fluss W vom Elternkreis zu diesem hin; negativ = in Gegenrichtung
ruhe Kreis blass zeichnen (nichts los)
zeichen kleine Plaketten: [{art, text?, titel?}] - art siehe
ENERGIEFLUSS_ZEICHEN in energiefluss.js
zustand was das Symbol braucht: {an, stand, laden, temps}
Vorzeichen wie im Manager: P_Grid > 0 Bezug, P_Akku > 0 Entladen,
P_Load und og negativ.
========================================================================== */
const ENERGIEFLUSS_KATALOG = (function () {
"use strict";
const kw = w => (Math.abs(w) / 1000).toFixed(1).replace(".", ",");
const wallboxZeichen = (modus, stecker, gesperrt) => {
const z = [];
if (modus === "Eco") z.push({ art: "eco", titel: "Eco-Modus" });
else if (modus === "Next Trip") z.push({ art: "uhr", titel: "Nächste Fahrt" });
else if (modus !== undefined) z.push({ art: "manuell", titel: "Standard-Modus" });
if (stecker) z.push({ art: "stecker", titel: "Fahrzeug angesteckt" });
if (gesperrt) z.push({ art: "schloss", titel: "Fahrzeug verriegelt" });
return z;
};
const knoten = [
// ---------------------------------------------------------------- Quellen
{
id: "pv", name: "Sonne", symbol: "sonne", farbe: "#f7c000",
eltern: "wr", x: 180, y: 130, skala: 15000,
klick: { ansicht: "anlage" },
// Welche Einträge von solarManager/P_PVn zu welcher Fläche gehören.
// Reihenfolge wie in solarManager.py.
optionen: {
flaechen: [
{ name: "Dach", von: 0, bis: 1, farbe: "#f7c000" },
{ name: "Veranda", von: 2, bis: 13, farbe: "#f78d00" },
{ name: "Carport", von: 14, bis: 33, farbe: "#f75a1e" },
],
},
lesen(m, opt) {
const pvn = m.json("solarManager/P_PVn") || [];
const pv = m.zahl("solarManager/P_PV");
const flaechen = (opt.flaechen || []).map(f => {
let w = 0;
for (let i = f.von; i <= f.bis && i < pvn.length; i++) w += Number(pvn[i]) || 0;
return { w, farbe: f.farbe, name: f.name };
});
return {
leistung: pv,
segmente: flaechen.length ? flaechen : null,
neben: flaechen.map(f => kw(f.w)).join(" · "),
fluss: -pv,
ruhe: pv < 20,
zustand: { an: pv >= 20, stand: Math.min(1, pv / 10000) },
};
},
},
{
id: "netz", name: "Netz", symbol: "netz", farbe: "#9a9aab",
eltern: "wr", x: 100, y: 380, skala: 15000,
lesen(m) {
const netz = m.zahl("solarManager/P_Grid");
const a = n => (Math.round(Math.abs(m.zahl("solarManager/i_" + n + "evu")) * 10) / 10).toString().replace(".", ",");
return {
leistung: Math.abs(netz),
neben: a("l1") + "·" + a("l2") + "·" + a("l3") + " A",
fluss: -netz,
ruhe: Math.abs(netz) < 30,
};
},
},
{
id: "akku", name: "Batterie", symbol: "akku", farbe: "#6cbe58",
eltern: "wr", x: 180, y: 630, skala: 15000,
lesen(m) {
const p = m.zahl("solarManager/P_Akku");
const soc = m.zahl("solarManager/SOC");
const grenze = m.zahl("solarManager/crgMaxPct");
const laden = p < -20;
const zeichen = [];
if (laden && grenze < 100) {
zeichen.push({ art: "uhr", text: Math.round(grenze) + " %", titel: "Ladeleistung begrenzt" });
}
return {
leistung: Math.abs(p),
neben: Math.round(soc) + " %" + (laden ? " · lädt" : p > 20 ? " · entlädt" : ""),
fluss: -p,
ruhe: Math.abs(p) < 20,
zeichen,
zustand: { stand: soc / 100, laden, an: Math.abs(p) >= 20 },
};
},
},
// ------------------------------------------------------------------ Mitte
{
id: "wr", name: "Autarkie", titel: "Wechselrichter (Autarkie)", symbol: "wechselrichter", farbe: "#b9c0c8",
eltern: null, pflicht: true, gross: true, x: 430, y: 380, skala: 100,
lesen(m) {
const aut = m.zahl("solarManager/autarky");
return {
leistung: aut,
wert: Math.round(aut) + " %",
neben: "Wirkungsgrad " + Math.round(m.zahl("solarManager/eff")) + " %",
segmente: [{ w: aut, farbe: "#6cbe58" }],
zustand: { an: m.zahl("solarManager/P_PV") >= 20 },
};
},
},
{
id: "verbrauch", name: "Verbrauch", symbol: "verbrauch", farbe: "#71afcd",
eltern: "wr", pflicht: true, gross: true, x: 770, y: 380, skala: 15000,
lesen(m) {
const last = -m.zahl("solarManager/P_Load");
const og = -m.zahl("solarManager/og");
const eg = m.zahl("solarManager/eg");
const ug = m.zahl("solarManager/ug");
const heiz = m.zahl("solarManager/pHeat");
const gemein = Math.max(0, last - ug - eg - og);
return {
leistung: last,
neben: "Gemein " + kw(gemein) + " kW",
segmente: [
{ w: og, farbe: "#70ccbb", knoten: "og" },
{ w: eg, farbe: "#70cc9c", knoten: "eg" },
{ w: ug, farbe: "#83a55b", knoten: "ug" },
{ w: heiz, farbe: "#c2614e", knoten: "heizstab" },
{ w: gemein, farbe: "#71afcd" },
],
fluss: last,
};
},
},
// --------------------------------------------------------------- Etagen
{
id: "og", name: "Obergeschoss", symbol: "etageOben", farbe: "#70ccbb",
eltern: "verbrauch", x: 920, y: 70, skala: 15000,
lesen(m) {
const w = -m.zahl("solarManager/og");
return {
leistung: w, fluss: w, ruhe: w < 20,
zeichen: m.zahl("solarManager/heatOG") ? [{ art: "heizung", titel: "Heizung an" }] : [],
};
},
},
{
id: "eg", name: "Erdgeschoss", symbol: "etageMitte", farbe: "#70cc9c",
eltern: "verbrauch", x: 920, y: 670, skala: 15000,
lesen(m) {
const w = m.zahl("solarManager/eg");
const wb = m.zahl("solarManager/evPower") * 1000;
return {
leistung: w, fluss: w, ruhe: w < 20,
segmente: [
{ w: Math.max(0, w - wb), farbe: "#70cc9c" },
{ w: Math.min(w, wb), farbe: "#60afbf", knoten: "wallboxEG" },
],
zeichen: m.zahl("solarManager/heatEG") ? [{ art: "heizung", titel: "Heizung an" }] : [],
};
},
},
{
id: "ug", name: "Untergeschoss", symbol: "etageUnten", farbe: "#83a55b",
eltern: "verbrauch", x: 600, y: 670, skala: 15000,
lesen(m) {
const w = m.zahl("solarManager/ug");
return { leistung: w, fluss: w, ruhe: w < 20 };
},
},
// ----------------------------------------------------------- Verbraucher
{
id: "heizstab", name: "Heizstab", symbol: "speicher", farbe: "#c2614e",
eltern: "verbrauch", x: 1120, y: 380, skala: 6000,
klick: { modal: "heater" },
lesen(m) {
const w = m.zahl("solarManager/pHeat");
const oben = m.zahl("solarManager/t_buffT");
const mitte = m.zahl("solarManager/t_buffM");
const unten = m.zahl("solarManager/t_buffB");
const eco = m.text("solarManager/heatMode") === "eco";
const grad = t => (Math.round(t * 10) / 10).toString().replace(".", ",");
return {
leistung: w, fluss: w, ruhe: w < 20,
neben: grad(oben) + " · " + grad(mitte) + " °C",
zeichen: [eco ? { art: "eco", titel: "Automatik (Überschuss)" } : { art: "manuell", titel: "Feste Leistung" }],
zustand: { an: w >= 20, temps: [oben, mitte, unten] },
};
},
},
{
id: "wallboxOG", name: "Wallbox OG", symbol: "wallbox", farbe: "#4b71a6",
eltern: "og", x: 1220, y: 70, skala: 11000,
klick: { modal: "CarOG" },
lesen(m) {
const w = m.zahl("solarManager/evPowerOG") * 1000;
const stecker = m.zahl("solarManager/evPlugOG") > 0;
return {
leistung: w, fluss: w, ruhe: w < 50 && !stecker,
neben: w >= 50 ? "lädt" : stecker ? "angesteckt" : "kein Auto",
zeichen: wallboxZeichen(m.text("solarManager/evModeOG"), stecker, false),
zustand: { an: w >= 50 },
};
},
},
{
id: "wallboxEG", name: "Wallbox EG", symbol: "wallbox", farbe: "#60afbf",
eltern: "eg", x: 1220, y: 670, skala: 11000,
klick: { modal: "CarEG" },
lesen(m) {
const w = m.zahl("solarManager/evPower") * 1000;
const soc = m.zahl("solarManager/evSOC");
const tank = m.zahl("solarManager/evFuel");
// solarManager/evPlug kommt als 1/0; aeltere Manager schickten den
// Text der Wallbox ("no car", "charging", ...).
const roh = m.text("solarManager/evPlug");
const stecker = isNaN(Number(roh)) ? (roh !== undefined && roh !== "no car") : Number(roh) > 0;
let neben = Math.round(soc) + " %";
const rest = m.zahl("solarManager/carRemChrg");
if (w >= 50 && rest > -1 && m.text("solarManager/carRemChrg") !== undefined) {
const fertig = new Date(Date.now() + rest * 60000);
neben = "fertig " + String(fertig.getHours()).padStart(2, "0") + ":" + String(fertig.getMinutes()).padStart(2, "0");
}
const zeichen = wallboxZeichen(m.text("solarManager/evMode"), stecker, m.zahl("solarManager/evLock") > 0);
if (tank > 0) zeichen.push({ art: "tank", text: Math.round(tank) + " %", titel: "Tank" });
return {
leistung: w, fluss: w, ruhe: w < 50 && !stecker,
neben, zeichen,
zustand: { an: w >= 50, stand: soc / 100 },
};
},
},
// ------------------------------------------------------------- frei
{
id: "bewaesserung", name: "Bewässerung", symbol: "tropfen", farbe: "#4b8fd6",
eltern: null, x: 550, y: 70,
klick: { modal: "watering" },
// Ids wie in wateringZones (solarMQTT.js) - dort werden Farbe, Text und
// Tooltip gesetzt, hier nur die Stellen dafür angelegt.
zonen: [
{ name: "Tröge", zeichen: "", status: "wateringTroughStatus", symbol: "wateringTroughIcon" },
{ name: "Vorn", zeichen: "", status: "wateringFrontStatus", symbol: "wateringFrontIcon" },
{ name: "Beete", zeichen: "", status: "wateringBedsStatus", symbol: "wateringBedsIcon" },
],
lesen() {
return { wert: "" };
},
},
];
return { knoten };
})();
+65 -236
View File
@@ -142,255 +142,84 @@ function setFlowWidth(id, width) {
flow.style.willChange = sichtbar ? "transform" : "auto"; flow.style.willChange = sichtbar ? "transform" : "auto";
} }
// Die Energiefluss-Uebersicht (js/solar/energiefluss.js) und der PV-Lageplan.
// Bis September 2026 stand hier updateValuesMQTT fuer das feste realtime.svg -
// jeder Kreis mit eigenen Zeilen. Was ein Kreis zeigt, steht jetzt im Katalog
// (energieflussKatalog.js); hier bleibt nur der Lageplan hinter der Sonne.
let energiefluss = null;
const solarSVG = { const solarSVG = {
updateCnt: 99, updateCnt: 99,
/** /**
* Beide Zwischenspeicher verwerfen - noetig, falls das SVG neu gezeichnet * Beide Zwischenspeicher verwerfen - noetig, wenn die Uebersicht neu
* wird. Nur die Knoten zu vergessen reicht nicht: letzterWert wuerde dann * gezeichnet wird (Wechsel quer/hochkant). Nur die Knoten zu vergessen
* jeden Schreibzugriff als "unveraendert" ueberspringen und das frische * reicht nicht: letzterWert wuerde dann jeden Schreibzugriff als
* SVG behielte seine Platzhalter. * "unveraendert" ueberspringen und die frischen Elemente behielten ihre
* Platzhalter.
*/ */
fillElementArray: function () { fillElementArray: function () {
for (const id in svgElements) delete svgElements[id]; for (const id in svgElements) delete svgElements[id];
letzterWert.clear(); letzterWert.clear();
}, },
updateValuesMQTT: function (mqttData) { updateValuesMQTT: function (mqttData) {
if (energiefluss) energiefluss.aktualisieren(mqttData);
const angleFactor = 15000 / 288; // Lageplan (assets/img/pvplan.svg): Leistung je Eingang und die Liste
const sixkWangleFactor = 6000 / 288; // der Wechselrichter.
var cons = Number(mqttData["solarManager/P_Load"]); let pvn = [];
var pvn = JSON.parse(mqttData["solarManager/P_PVn"]); try { pvn = JSON.parse(mqttData["solarManager/P_PVn"]); } catch (e) { pvn = []; }
var pv = Number(mqttData["solarManager/P_PV"]); for (let i = 0; i < pvn.length; i++) {
var pv2 = Number(pvn[1]); if (svgElement("det_pv" + i + "P")) setText("det_pv" + i + "P", powerToString(pvn[i]));
var pv1 = Number(pvn[0]); }
var pv3 = pv - (pv1 + pv2); let liste = "";
var pbatt = Number(mqttData["solarManager/P_Akku"]); for (let i = 0; mqttData["solarManager/inverters" + i + "/name"] !== undefined; i++) {
var limbatt = Number(mqttData["solarManager/crgMaxPct"]); const basis = "solarManager/inverters" + i;
var grid = Number(mqttData["solarManager/P_Grid"]); const gestoert = mqttData[basis + "/error"] > 0;
var soc = Number(mqttData["solarManager/SOC"]); liste += '<tspan x="0" dy="1.2em">' + mqttData[basis + "/name"] + ":</tspan>"
var og = Number(mqttData["solarManager/og"]); + '<tspan x="160">' + (gestoert ? "--" : Math.round(mqttData[basis + "/temp"])) + " °C</tspan>"
var eg = Number(mqttData["solarManager/eg"]); + '<tspan x="215">' + (gestoert ? "-- W" : powerToString(mqttData[basis + "/p_AC"])) + "</tspan>";
var ug = Number(mqttData["solarManager/ug"]); }
var evsoc = Number(mqttData["solarManager/evSOC"]); if (svgElement("invList")) setHTML("invList", liste);
var evlock = Number(mqttData["solarManager/evLock"]); }
var evMode = mqttData["solarManager/evMode"]; };
var evPower = Number(mqttData["solarManager/evPower"]);
var plugev = Number(mqttData["solarManager/evPlug"]);
var fuelev = Number(mqttData["solarManager/evFuel"]);
var heatOG = Number(mqttData["solarManager/heatOG"]);
var heatEG = Number(mqttData["solarManager/heatEG"]);
var puffO = Number(mqttData["solarManager/t_buffT"]);
var puffM = Number(mqttData["solarManager/t_buffM"]);
var puffU = Number(mqttData["solarManager/t_buffB"]);
var heatMode = mqttData["solarManager/heatMode"];
var carRemChrg = Number(mqttData["solarManager/carRemChrg"]);
var aut = Number(mqttData["solarManager/autarky"]);
var Pheat = Number(mqttData["solarManager/pHeat"]);
var waterHeight = Number(mqttData["solarManager/waterHeight"]);
var waterTemp = Number(mqttData["solarManager/waterTemp"]);
var p_wr = Number(mqttData["solarManager/P_WR"]);
var eff = Number(mqttData["solarManager/eff"]);
var i_l1evu = Number(mqttData["solarManager/i_l1evu"]);
var i_l2evu = Number(mqttData["solarManager/i_l2evu"]);
var i_l3evu = Number(mqttData["solarManager/i_l3evu"]);
var evPowerOG = Number(mqttData["solarManager/evPowerOG"]);
var evPlugOG = Number(mqttData["solarManager/evPlugOG"]);
var evModeOG = mqttData["solarManager/evModeOG"];
/** Energiefluss aufbauen und den Umschalter Uebersicht/Anlage verdrahten. */
function energieflussStarten() {
const behaelter = document.getElementById("energiefluss");
if (!behaelter || typeof Energiefluss === "undefined") return;
var common = -cons - ug - eg + og ; const zeigeAnsicht = function (name) {
if(common < 0){ document.querySelectorAll("[data-enf-ansicht]").forEach(function (knopf) {
common = 0; const an = knopf.dataset.enfAnsicht === name;
knopf.classList.toggle("active", an);
knopf.setAttribute("aria-selected", an ? "true" : "false");
});
document.querySelectorAll("[data-enf-ansicht-flaeche]").forEach(function (flaeche) {
const an = flaeche.dataset.enfAnsichtFlaeche === name;
flaeche.hidden = !an;
if (an) {
flaeche.classList.remove("enf-rein");
void flaeche.offsetWidth;
flaeche.classList.add("enf-rein");
} }
});
};
document.querySelectorAll("[data-enf-ansicht]").forEach(function (knopf) {
knopf.addEventListener("click", function () { zeigeAnsicht(knopf.dataset.enfAnsicht); });
});
setAttr('consumerArc', "d", describeArc(100, 100, 95, 0, Math.round(-cons / angleFactor))); energiefluss = new Energiefluss(behaelter, {
einstellungen: typeof energieflussEinstellungen !== "undefined" ? energieflussEinstellungen : {},
setAttr("ogArc", "d", describeArc(100, 100, 95, 0, Math.round(-og / angleFactor))); onKlick: function (knoten) {
setText("consumerText", powerToString(-cons)); if (knoten.klick.ansicht) zeigeAnsicht(knoten.klick.ansicht);
else if (knoten.klick.modal) openModal(knoten.klick.modal);
setText("consumerTextAllg", "Gemein: "+powerToString(common)); },
onGezeichnet: function () {
if (evPower > 0) { solarSVG.fillElementArray();
setDisplay("evCharge", ""); if (typeof updateWateringStatus === "function") updateWateringStatus();
if (carRemChrg > -1) { },
var today = new Date(); });
today.setMinutes(today.getMinutes() + carRemChrg); energiefluss.zeichnen();
var h = today.getHours();
var m = today.getMinutes();
if (h < 10)
h = "0" + h;
if (m < 10)
m = "0" + m;
setText("evRemText", h + ":" + m);
}
} else {
setText("evRemText", "");
}
setText("ogText", powerToString(-og));
setText("egText", powerToString(eg));
setText("ugText", powerToString(ug));
setText("pvText", powerToString(pv));
setText("pv1txt", powerToString(pv1));
setText("pv2txt", powerToString(pv2));
setText("pv3txt", powerToString(pv3));
setText("il1txt", Math.round(i_l1evu * 10) / 10 + "A");
setText("il2txt", Math.round(i_l2evu * 10) / 10 + "A");
setText("il3txt", Math.round(i_l3evu * 10) / 10 + "A");
setText("batText", powerToString(pbatt));
setText("heatText", powerToString(Pheat));
setText("evText", powerToString(evPower * 1000));
setText("evTextOG", powerToString(evPowerOG * 1000));
setText("genText", "Autarkie: " + Math.round(aut) + "%");
setText("genInfoText", "Wirkungsgrad: " + Math.round(eff) + "%");// powerToString(p_wr);
setText("gridText", powerToString(grid));
for (i = 0; i < pvn.length; i++) {
setText("det_pv" + i + "P", powerToString(pvn[i]));
}
tttext = ""
var i = 0;
while (mqttData["solarManager/inverters" + i]) {
if (mqttData["solarManager/inverters" + i+"/error"] > 0) {
tttext = tttext + '<tspan x="0" dy="1.2em">' + mqttData["solarManager/inverters" + i+"/name"] + ":</tspan>"
tttext = tttext + '<tspan x="160">' + "--" + " °C</tspan>"
tttext = tttext + '<tspan x="215">' + "--W" + "</tspan>"
} else {
tttext = tttext + '<tspan x="0" dy="1.2em">' + mqttData["solarManager/inverters" + i+"/name"] + ":</tspan>"
tttext = tttext + '<tspan x="160">' + Math.round(mqttData["solarManager/inverters" + i+"/temp"]) + " °C</tspan>"
tttext = tttext + '<tspan x="215">' + powerToString(mqttData["solarManager/inverters" + i+"/p_AC"]) + "</tspan>"
}
i++;
}
setHTML("invList", tttext);
setAttr("egArc", "d", describeArc(100, 100, 95, 0, Math.round(eg / angleFactor)));
setAttr("ugArc", "d", describeArc(100, 100, 95, 0, Math.round(ug / angleFactor)));
setAttr("pvArc", "d", describeArc(100, 100, 95, 0, Math.round(pv1 / angleFactor)));
setAttr("pv2Arc", "d", describeArc(100, 100, 95, Math.round(pv1 / angleFactor), Math.round(pv1 / angleFactor) + Math.round(pv2 / angleFactor)));
setAttr("pv3Arc", "d", describeArc(100, 100, 95, Math.round(pv1 / angleFactor) + Math.round(pv2 / angleFactor), Math.round(pv1 / angleFactor) + Math.round(pv2 / angleFactor) + Math.round(pv3 / angleFactor)));
if (pbatt < 0) {
if (limbatt < 100) {
setDisplay("charge", "none");
setDisplay("batLim", "");
setText("batLimText", Math.round(limbatt) + "%");
} else {
setDisplay("charge", "");
setDisplay("batLim", "none");
}
setAttr("batArc", "d", describeArc(100, 100, 95, 0, Math.round(-pbatt / angleFactor)));
setAttr("battani", "class", "stream-rev");
setFlowWidth("battani", 55 * (1 - Math.exp(pbatt / 2000)));
} else {
setDisplay("charge", "none");
setDisplay("batLim", "none");
setAttr("batArc", "d", describeArc(100, 100, 95, 0, Math.round(pbatt / angleFactor)));
setAttr("battani", "class", "stream");
setFlowWidth("battani", 55 * (1 - Math.exp(-pbatt / 2000)));
}
setAttr("batChargeState", "y", (100 - soc) + "%");
//setAttr("heatChargeState", "y", 100 - ((((puffO + puffM) / 2) - 30) * 1.66) + "%");
setAttr('tmpH', "stop-color", assignColor("#2389BA", "#BA3B23", 20, 80, puffO));
setAttr('tmpM', "stop-color", assignColor("#2389BA", "#BA3B23", 20, 80, puffM));
setAttr('tmpL', "stop-color", assignColor("#2389BA", "#BA3B23", 20, 80, puffU));
setText("batSOC", soc + "%");
setAttr("evArc", "d", describeArc(100, 100, 95, 0, Math.round(evPower * 2000 / angleFactor)));
setAttr("evChargeState", "y", (100 - evsoc) + "%");
setText("evSOC", evsoc + "%");
setText("evFuel", fuelev + "%");
setAttr("heatArc", "d", describeArc(100, 100, 95, 0, Math.round(Pheat / sixkWangleFactor)));
setText("heatUp", Math.round(puffO * 10) / 10 + " °C");
setText("heatDown", Math.round(puffM * 10) / 10 + " °C");
if (evlock == false) {
setDisplay("evLock", "none");
} else {
setDisplay("evLock", "");
}
if (heatMode == "eco") {
setDisplay("heatEco", "");
setDisplay("heatDefault", "none");
} else {
setDisplay("heatEco", "none");
setDisplay("heatDefault", "");
}
if (evMode == "Eco") {
setDisplay("evEco", "");
setDisplay("evNextTrip", "none");
setDisplay("evDefault", "none");
} else if (evMode == "Next Trip") {
setDisplay("evEco", "none");
setDisplay("evNextTrip", "");
setDisplay("evDefault", "none");
} else {
setDisplay("evEco", "none");
setDisplay("evNextTrip", "none");
setDisplay("evDefault", "");
}
if (heatOG) {
setDisplay("heatOG", "");
} else {
setDisplay("heatOG", "none");
}
if (heatEG) {
setDisplay("heatEG", "");
} else {
setDisplay("heatEG", "none");
}
// solarManager/evPlug kommt als 1 oder 0, wie evPlugOG. Frueher kam
// hier der Text der Wallbox an ("no car", "charging", ...); Number()
// machte daraus NaN, und NaN != "no car" ist immer wahr - das
// Steckersymbol stand dauerhaft da. Den Text weiter zu verstehen
// kostet eine Zeile und haelt die Anzeige richtig, egal ob der
// Manager schon neu gestartet ist oder nicht.
const plugText = mqttData["solarManager/evPlug"];
if (isNaN(plugev) ? (plugText !== undefined && plugText !== "no car") : plugev > 0) {
setDisplay("evPlug", "");
} else {
setDisplay("evPlug", "none");
}
if (evModeOG == "Eco") {
setDisplay("evEcoOG", "");
setDisplay("evNextTripOG", "none");
setDisplay("evDefaultOG", "none");
} else if (evModeOG == "Next Trip") {
setDisplay("evEcoOG", "none");
setDisplay("evNextTripOG", "");
setDisplay("evDefaultOG", "none");
} else {
setDisplay("evEcoOG", "none");
setDisplay("evNextTripOG", "none");
setDisplay("evDefaultOG", "");
}
if (evPlugOG > 0) {
setDisplay("evPlugOG", "");
} else {
setDisplay("evPlugOG", "none");
}
if (grid < 0) {
setAttr("gridArc", "d", describeArc(100, 100, 95, 0, -grid / angleFactor));
setAttr("consGridani", "class", "stream-rev");
setFlowWidth("consGridani", 55 * (1 - Math.exp(grid / 2000)));
} else {
setAttr("gridArc", "d", describeArc(100, 100, 95, 0, grid / angleFactor));
setAttr("consGridani", "class", "stream");
setFlowWidth("consGridani", 55 * (1 - Math.exp(-grid / 2000)));
}
setFlowWidth("consEVani", 55 * (1 - Math.exp(-evPower)));
setFlowWidth("consEVOGani", 55 * (1 - Math.exp(-evPowerOG)));
setFlowWidth("consOGani", 55 * (1 - Math.exp(og / 2000)));
setFlowWidth("consUGani", 55 * (1 - Math.exp( -ug / 2000)));
setFlowWidth("consHeatAni", 55 * (1 - Math.exp(-Pheat / 2000)));
setFlowWidth("consAni", 55 * (1 - Math.exp(cons / 2000)));
setFlowWidth("consEGani", 55 * (1 - Math.exp(-eg / 2000)));
setFlowWidth("pvani", 55 * (1 - Math.exp(-pv / 2000)));
}
} }
var chartSettings = { var chartSettings = {
@@ -604,7 +433,7 @@ var chartSettings = {
// hier ist sie Beiwerk, dort der Zweck der Seite. // hier ist sie Beiwerk, dort der Zweck der Seite.
baueJahresstatistik(document.getElementById("Stats-Year"), baueJahresstatistik(document.getElementById("Stats-Year"),
{ spalten: 1, werkzeuge: false, fussnoten: false }); { spalten: 1, werkzeuge: false, fussnoten: false });
solarSVG.fillElementArray(); energieflussStarten();
mountMeteogram(); mountMeteogram();
} }
}); });
+105
View File
@@ -0,0 +1,105 @@
<?php
/**
* Einstellungen der Energiefluss-Übersicht (Karte "Realtime" auf der
* Solar-Seite), Tabelle homeMesh.energiefluss - siehe homeMesh_energiefluss.sql.
*
* Welche Kreise es gibt und wie sie gezeichnet werden, weiß allein der
* Katalog in js/solar/energieflussKatalog.js. Hier wird nur gespeichert, was
* ein Haus davon abweichend einstellt. PHP kennt deshalb die Kennungen nicht
* und prüft nur ihre Form - ein neuer Kreis im Katalog braucht hier keine
* Zeile.
*/
require_once(__DIR__ . "/meshdb.php");
/**
* Alle Abweichungen, je Kennung.
*
* Fehlt die Tabelle (Installation ohne homeMesh_energiefluss.sql), kommt ein
* leeres Ergebnis zurück: die Übersicht zeichnet dann den Katalog, statt die
* Solar-Seite mit einem Fehler zu verlieren.
*
* @return array<string, array{aktiv: bool, name: ?string, x: ?int, y: ?int, optionen: ?array}>
*/
function energieflussLaden()
{
$ergebnis = [];
try {
$res = meshDb()->query("SELECT id, aktiv, name, x, y, optionen FROM energiefluss");
} catch (Throwable $e) {
return $ergebnis;
}
while ($res && $z = $res->fetch_assoc()) {
$optionen = $z["optionen"] !== null ? json_decode($z["optionen"], true) : null;
$ergebnis[$z["id"]] = [
"aktiv" => (bool)$z["aktiv"],
"name" => $z["name"],
"x" => $z["x"] !== null ? (int)$z["x"] : null,
"y" => $z["y"] !== null ? (int)$z["y"] : null,
"optionen" => is_array($optionen) ? $optionen : null,
];
}
return $ergebnis;
}
/**
* Den ganzen Stand ersetzen.
*
* Die Maske schickt immer alle Kreise, auch unveränderte - so gibt es keinen
* Zwischenstand, in dem die Hälfte gespeichert ist. Einträge, die dem Katalog
* entsprechen (aktiv, ohne Name, Lage und Optionen), werden gar nicht erst
* geschrieben: die Tabelle hält nur echte Abweichungen.
*
* @param array $knoten [{id, aktiv, name, x, y, optionen}, ...]
* @return int Anzahl gespeicherter Abweichungen
*/
function energieflussSpeichern($knoten)
{
if (!is_array($knoten)) {
throw new InvalidArgumentException("Keine Kreise übergeben.");
}
$zeilen = [];
foreach ($knoten as $k) {
$id = (string)($k["id"] ?? "");
if (!preg_match('/^[A-Za-z][A-Za-z0-9_]{0,19}$/', $id)) {
throw new InvalidArgumentException("Ungültige Kennung: " . $id);
}
$name = trim((string)($k["name"] ?? ""));
if (mb_strlen($name) > 40) {
throw new InvalidArgumentException("Der Name „" . $name . "“ ist länger als 40 Zeichen.");
}
$x = isset($k["x"]) && $k["x"] !== null ? (int)round($k["x"]) : null;
$y = isset($k["y"]) && $k["y"] !== null ? (int)round($k["y"]) : null;
foreach ([$x, $y] as $w) {
if ($w !== null && ($w < -2000 || $w > 4000)) {
throw new InvalidArgumentException("Die Lage von „" . $id . "“ liegt außerhalb der Zeichenfläche.");
}
}
$optionen = $k["optionen"] ?? null;
if ($optionen !== null && !is_array($optionen)) {
throw new InvalidArgumentException("Optionen von „" . $id . "“ sind kein Objekt.");
}
$aktiv = !isset($k["aktiv"]) || (bool)$k["aktiv"];
if ($aktiv && $name === "" && $x === null && $y === null && $optionen === null) {
continue;
}
$zeilen[] = [$id, $aktiv ? 1 : 0, $name === "" ? null : $name, $x, $y,
$optionen === null ? null : json_encode($optionen, JSON_UNESCAPED_UNICODE)];
}
$db = meshDb();
$db->begin_transaction();
try {
$db->query("DELETE FROM energiefluss");
$st = $db->prepare("INSERT INTO energiefluss (id, aktiv, name, x, y, optionen) VALUES (?, ?, ?, ?, ?, ?)");
foreach ($zeilen as $z) {
$st->bind_param("sisiis", $z[0], $z[1], $z[2], $z[3], $z[4], $z[5]);
$st->execute();
}
$db->commit();
} catch (Throwable $e) {
$db->rollback();
throw new RuntimeException("Die Übersicht wurde nicht gespeichert: " . $e->getMessage());
}
return count($zeilen);
}
+43
View File
@@ -33,6 +33,7 @@ require_once(__DIR__ . "/skodaKeys.php");
$reiter = [ $reiter = [
["id" => "preise", "titel" => "Preise", "symbol" => "bi-currency-euro"], ["id" => "preise", "titel" => "Preise", "symbol" => "bi-currency-euro"],
["id" => "grundriss", "titel" => "Grundriss", "symbol" => "bi-house"], ["id" => "grundriss", "titel" => "Grundriss", "symbol" => "bi-house"],
["id" => "energiefluss", "titel" => "Übersicht", "symbol" => "bi-diagram-3"],
["id" => "kacheln", "titel" => "Home-Kacheln", "symbol" => "bi-grid-1x2"], ["id" => "kacheln", "titel" => "Home-Kacheln", "symbol" => "bi-grid-1x2"],
["id" => "geraete", "titel" => "Geräte", "symbol" => "bi-house-gear"], ["id" => "geraete", "titel" => "Geräte", "symbol" => "bi-house-gear"],
["id" => "fahrzeug", "titel" => "Fahrzeug", "symbol" => "bi-ev-front"], ["id" => "fahrzeug", "titel" => "Fahrzeug", "symbol" => "bi-ev-front"],
@@ -162,6 +163,48 @@ try {
</div> </div>
<?= reiterFlaecheZu() ?><!-- /Grundriss --> <?= reiterFlaecheZu() ?><!-- /Grundriss -->
<!-- ================================================== Übersicht ==== -->
<!-- Die Energiefluss-Übersicht der Solar-Seite. Welche Kreise es gibt,
steht im Katalog (js/solar/energieflussKatalog.js); hier wird nur
eingestellt, was dieses Haus davon abweicht - gespeichert in
homeMesh.energiefluss. Vorschau und Liste zeichnet
js/solar/energieflussEinstellungen.js. -->
<?= reiterFlaecheAuf("energiefluss", $reiterOpt) ?>
<div class="card mb-4" id="energieflussKarte">
<div class="card-header pb-2 pt-2">
<h3 class="card-title"><i class="bi bi-diagram-3 me-2"></i>Energiefluss-Übersicht</h3>
<div class="card-tools">
<button type="button" class="btn btn-tool" id="energieflussNeuLaden" title="Änderungen verwerfen">
<i class="bi bi-arrow-counterclockwise"></i>
</button>
</div>
</div>
<div class="card-body">
<p class="text-secondary small mb-3">
Kreise, die es in diesem Haus nicht gibt, werden ausgeschaltet hängen andere
daran, rücken sie eine Ebene nach oben. In der Vorschau lassen sich die Kreise
mit der Maus verschieben. Auf schmalen Bildschirmen dreht die Übersicht sich
von selbst hochkant. Die Werte in der Vorschau sind Beispiele.
</p>
<div class="row g-4">
<div class="col-12 col-xl-7">
<div class="enf-vorschau" id="energieflussVorschau"></div>
</div>
<div class="col-12 col-xl-5">
<div id="energieflussListe">
<p class="text-secondary small mb-0">Wird geladen </p>
</div>
</div>
</div>
</div>
<div class="card-footer d-flex flex-wrap align-items-center gap-3">
<button type="button" class="btn btn-primary" id="energieflussSpeichern">Speichern</button>
<button type="button" class="btn btn-outline-secondary" id="energieflussZuruecksetzen">Alles auf Katalog zurücksetzen</button>
<span class="small" id="energieflussHinweis"></span>
</div>
</div>
<?= reiterFlaecheZu() ?><!-- /Übersicht -->
<!-- ================================================ Home-Kacheln ==== --> <!-- ================================================ Home-Kacheln ==== -->
<!-- Der Inhalt kommt aus js/solar/settings.js: eine Karte je Etage, darin <!-- Der Inhalt kommt aus js/solar/settings.js: eine Karte je Etage, darin
je Raum eine Zeile mit ihren Werten. Gezeichnet wird aus dem Modell, je Raum eine Zeile mit ihren Werten. Gezeichnet wird aus dem Modell,
+24 -9
View File
@@ -1,3 +1,4 @@
<?php require_once(__DIR__ . "/energiefluss.php"); ?>
<style> <style>
.datepicker-native { .datepicker-native {
position: relative; position: relative;
@@ -47,22 +48,36 @@
<!--begin::Row--> <!--begin::Row-->
<div class="row"> <div class="row">
<!-- Start col --> <!-- Start col -->
<!-- Energiefluss. Gezeichnet von js/solar/energiefluss.js aus dem Katalog
(energieflussKatalog.js) und den Einstellungen dieses Hauses
(Einstellungen -> Übersicht). Bis September 2026 stand hier
assets/img/realtime.svg mit festen Kreisen. -->
<div class="col-xxl-12 col-xl-12 d-flex align-items-stretch"> <div class="col-xxl-12 col-xl-12 d-flex align-items-stretch">
<div class="card p-0 mb-4" style="width:100%"> <div class="card p-0 mb-4" style="width:100%" id="energieflussKarte">
<div class="card-header pb-0 pt-1"> <div class="card-header pb-1 pt-1 d-flex align-items-center">
<h3 class="card-title">Realtime</h3> <h3 class="card-title me-auto">Realtime</h3>
<div class="enf-umschalter me-2" role="tablist" aria-label="Ansicht">
<button type="button" class="btn btn-sm active" role="tab" aria-selected="true" data-enf-ansicht="fluss">Übersicht</button>
<button type="button" class="btn btn-sm" role="tab" aria-selected="false" data-enf-ansicht="anlage">Anlage</button>
</div>
<div class="card-tools"> <div class="card-tools">
<button type="button" class="btn btn-tool" data-lte-toggle="card-remove"> <a class="btn btn-tool" href="index.php?action=settings#energiefluss" title="Übersicht einstellen">
<i class="bi bi-x-lg"></i> <i class="bi bi-sliders"></i>
</button> </a>
</div> </div>
</div> </div>
<div class="card-body p-0 d-flex flex-column" id="realtimeSVGcontainer" style="position:relative;max-height:800px;"> <div class="card-body p-2 p-md-3">
<?php readfile(__DIR__ . "/../assets/img/realtime.svg"); ?> <div class="enf-ansicht enf-behaelter" data-enf-ansicht-flaeche="fluss" id="energiefluss"></div>
<div class="enf-ansicht" data-enf-ansicht-flaeche="anlage" hidden>
<?php readfile(__DIR__ . "/../assets/img/pvplan.svg"); ?>
</div> </div>
</div> </div>
<!-- /.card -->
</div> </div>
</div>
<script>
// Abweichungen dieses Hauses vom Katalog, siehe restricted/energiefluss.php.
const energieflussEinstellungen = <?= json_encode((object)energieflussLaden(), JSON_UNESCAPED_UNICODE) ?>;
</script>
<div class="col-xxl-12 col-xl-12 col-lg-12 col-md-12 d-flex align-items-stretch"> <div class="col-xxl-12 col-xl-12 col-lg-12 col-md-12 d-flex align-items-stretch">
<div class="card p-0 mb-4" style="width:100%"> <div class="card p-0 mb-4" style="width:100%">