Solar: Anlage-Ansicht neu, im Stil der Energiefluss-Uebersicht
Der alte Plan (pvplan.svg) ist ersetzt durch einen Katalog der Anlage (js/solar/anlageKatalog.js: Flaechen, Wechselrichter, echte Lage jeder Platte) und einen Renderer (js/solar/anlage.js): - Flaechen mit Leistung und Anteil als Balken - Lageplan: Farbe stufenlos nach Leistung im Vergleich zu den staerksten Platten der Flaeche, Warnzeichen nur bei genug Licht unter 70 % des Medians, "ohne Daten" wenn der Wechselrichter nichts meldet; Auftritt, Lichtwelle bei Sonne, pulsierende Warnung; hochkant gedreht auf schmalen Schirmen - Detail zu Platte, String oder Wechselrichter (Eingaenge als Balken, Auslastung, Temperatur), Verlauf je Platte ab Seitenaufruf - Wechselrichterliste mit Zustand ok / hochgerechnet / keine Daten Die Flaechen im Sonnenring der Uebersicht kommen jetzt aus demselben Katalog. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -303,8 +303,7 @@ Kilowatt melden — die Wallboxen), runden (`stellen`), Einheit anhängen. Klick
|
|||||||
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 Solar-Seite. Startet die Energiefluss-Übersicht
|
**`solarMQTT.js`** — die Solar-Seite. Startet die Energiefluss-Übersicht
|
||||||
(`energieflussStarten()`), füllt den PV-Lageplan hinter der Sonne
|
(`energieflussStarten()`) samt Anlage-Ansicht, füllt die Bewässerungszonen,
|
||||||
(`assets/img/pvplan.svg`, Ids `det_pvNP`, `invList`) und die Bewässerungszonen,
|
|
||||||
darunter hängen die Diagramme Prognose, Verbrauch und Erzeugung über
|
darunter hängen die Diagramme Prognose, Verbrauch und Erzeugung über
|
||||||
`getData()`. Aktualisiert wird höchstens einmal je Sekunde
|
`getData()`. Aktualisiert wird höchstens einmal je Sekunde
|
||||||
(`solarSvgUpdatePending`); die Setter (`setText`, `setAttr`) schreiben nur,
|
(`solarSvgUpdatePending`); die Setter (`setText`, `setAttr`) schreiben nur,
|
||||||
@@ -328,6 +327,21 @@ still, solange die Karte nicht zu sehen ist, und entfallen bei
|
|||||||
`css/solar.css`. Bis September 2026 stand hier `assets/img/realtime.svg` mit
|
`css/solar.css`. Bis September 2026 stand hier `assets/img/realtime.svg` mit
|
||||||
festen Kreisen.
|
festen Kreisen.
|
||||||
|
|
||||||
|
**Anlage-Ansicht** (Umschalter „Anlage“ oder Klick auf die Sonne) — dieselbe
|
||||||
|
Arbeitsteilung:
|
||||||
|
|
||||||
|
| Datei | enthält |
|
||||||
|
|---|---|
|
||||||
|
| `anlageKatalog.js` | die PV-Anlage dieses Hauses: Flächen mit Umriss, Wechselrichter (`invertersN` und ihre Eingänge in `P_PVn`), Strings ohne Einzelwerte (Gen24) und jede Platte mit ihrer echten Lage. Auch die Flächen im Sonnenring der Übersicht leiten sich daraus ab. |
|
||||||
|
| `anlage.js` | den Renderer (`new Anlage(behälter)`): Flächen mit Leistungsanteil, Lageplan (hochkant gedreht unter 800 px), Detail zu Platte, String oder Wechselrichter, Wechselrichterliste mit Auslastung und Zustand. |
|
||||||
|
|
||||||
|
Die Farbe einer Platte zeigt ihre Leistung im Vergleich zu den stärksten
|
||||||
|
ihrer Fläche (90. Perzentil) — nur Anzeige, kein Urteil. Ein Warnzeichen
|
||||||
|
bekommt sie erst, wenn sie bei genug Licht (Flächenmedian ab 30 W) unter 70 %
|
||||||
|
des Medians liegt. Meldet ein Wechselrichter nichts, stehen seine Platten auf
|
||||||
|
„ohne Daten“ statt „auffällig“. Den Verlauf je Platte gibt es nicht in der
|
||||||
|
Datenbank; die Ansicht schreibt ihn ab dem Seitenaufruf mit (40 Minuten).
|
||||||
|
|
||||||
**`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
|
||||||
als SVG-Draufsicht mit Ladestand im Akku, Ladeknopf und Klimaknopf, und die
|
als SVG-Draufsicht mit Ladestand im Akku, Ladeknopf und Klimaknopf, und die
|
||||||
|
|||||||
@@ -1,192 +0,0 @@
|
|||||||
<!-- 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>
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB |
+139
@@ -3594,6 +3594,145 @@ svg.enf.enf-hoch { max-height: none; }
|
|||||||
.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 { 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; }
|
.enf-e-flaeche .form-control.form-control-color { width: 2.2rem; height: 1.9rem; padding: .15rem; }
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Anlage-Ansicht (js/solar/anlage.js, Katalog js/solar/anlageKatalog.js).
|
||||||
|
Dieselben Töne wie die Energiefluss-Übersicht.
|
||||||
|
========================================================================== */
|
||||||
|
.anl { display: grid; gap: .9rem; }
|
||||||
|
.anl .anl-flaechen { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: .6rem; }
|
||||||
|
.anl .anl-flaeche { background: var(--ton-grund, #191d20); border-radius: .8rem; padding: .6rem .85rem .55rem; display: grid; gap: .35rem; }
|
||||||
|
.anl .anl-flaeche-kopf { display: flex; align-items: baseline; gap: .5rem; }
|
||||||
|
.anl .anl-flaeche-kopf i { width: .6rem; height: .6rem; border-radius: 50%; background: var(--farbe); align-self: center; box-shadow: 0 0 0 3px color-mix(in srgb, var(--farbe) 25%, transparent); }
|
||||||
|
.anl .anl-flaeche-kopf span { font-weight: 600; color: var(--ton-schrift, #e8ede9); }
|
||||||
|
.anl .anl-flaeche-kopf b { margin-left: auto; font-size: 1.2rem; font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||||
|
.anl .anl-flaeche-balken { height: 5px; border-radius: 3px; background: #2b3139; overflow: hidden; }
|
||||||
|
.anl .anl-flaeche-balken span { display: block; height: 100%; background: var(--farbe); border-radius: 3px; transition: width 1s cubic-bezier(.2, .7, .2, 1); }
|
||||||
|
.anl .anl-flaeche small { color: var(--ton-schrift-3, #6f7a81); font-size: .76rem; }
|
||||||
|
@media (max-width: 575.98px) {
|
||||||
|
/* Auf dem Handy drei schmale Kacheln nebeneinander statt drei Zeilen. */
|
||||||
|
.anl .anl-flaechen { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .4rem; }
|
||||||
|
.anl .anl-flaeche { padding: .45rem .55rem; }
|
||||||
|
.anl .anl-flaeche-kopf { flex-wrap: wrap; gap: .1rem .4rem; }
|
||||||
|
.anl .anl-flaeche-kopf span { font-size: .8rem; }
|
||||||
|
.anl .anl-flaeche-kopf b { margin-left: 0; width: 100%; font-size: 1rem; }
|
||||||
|
.anl .anl-flaeche small { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.anl .anl-plan-rahmen { background: var(--ton-grund, #191d20); border-radius: .9rem; padding: .4rem; }
|
||||||
|
svg.anl-plan { display: block; width: 100%; height: auto; max-height: 72vh; font-family: "Poppins", system-ui, sans-serif; user-select: none; -webkit-tap-highlight-color: transparent; }
|
||||||
|
svg.anl-plan.anl-hoch { max-height: none; }
|
||||||
|
|
||||||
|
.anl-plan .anl-dachflaeche { fill: var(--farbe); fill-opacity: .06; stroke: var(--farbe); stroke-opacity: .18; stroke-width: 22; stroke-linejoin: round; }
|
||||||
|
.anl-plan .anl-bereich { fill: var(--farbe); fill-opacity: .035; stroke: var(--farbe); stroke-opacity: .25; stroke-width: 2; stroke-dasharray: 7 7; }
|
||||||
|
|
||||||
|
.anl-plan .anl-teil { transform-box: fill-box; transform-origin: center; animation: anl-auf .55s cubic-bezier(.2, .9, .3, 1.2) backwards; animation-delay: calc(var(--d) * 1ms); }
|
||||||
|
@keyframes anl-auf { from { opacity: 0; transform: scale(.4); } }
|
||||||
|
.anl-plan .anl-koerper { fill: #2f353c; stroke: rgba(255, 255, 255, .1); stroke-width: 1; transition: fill 1s, stroke .2s, stroke-width .2s; }
|
||||||
|
.anl-plan .anl-platte .anl-koerper,
|
||||||
|
.anl-plan .anl-string .anl-koerper { fill: var(--fuellung, #3b3a32); }
|
||||||
|
.anl-plan .anl-platte.anl-offline .anl-koerper { fill: #262b31; stroke: #5c646d; stroke-dasharray: 4 3; }
|
||||||
|
.anl-plan .anl-zellen,
|
||||||
|
.anl-plan .anl-glanz { pointer-events: none; }
|
||||||
|
.anl-plan .anl-glanz { fill: #fff; opacity: 0; }
|
||||||
|
/* Lichtwelle: jede Platte hellt kurz auf, versetzt nach ihrer Lage. */
|
||||||
|
.anl-plan.anl-sonnig .anl-hell .anl-glanz {
|
||||||
|
animation: anl-glanz 7s ease-in-out infinite; animation-delay: calc(var(--glanz) * 7s); }
|
||||||
|
@keyframes anl-glanz { 0%, 100% { opacity: 0; } 7% { opacity: .3; } 15% { opacity: 0; } }
|
||||||
|
|
||||||
|
.anl-plan .anl-platte,
|
||||||
|
.anl-plan .anl-string { cursor: pointer; }
|
||||||
|
.anl-plan .anl-platte:hover .anl-koerper,
|
||||||
|
.anl-plan .anl-string:hover .anl-koerper { stroke: rgba(255, 255, 255, .7); stroke-width: 2; }
|
||||||
|
.anl-plan .anl-geschwister .anl-koerper { stroke: #e3e6ea; stroke-width: 2; stroke-dasharray: 6 4; }
|
||||||
|
.anl-plan .anl-gewaehlt .anl-koerper { stroke: #fff; stroke-width: 3.5; stroke-dasharray: none; }
|
||||||
|
|
||||||
|
.anl-plan .anl-w { font-size: 17px; font-weight: 600; fill: #c9d0d6; text-anchor: middle; dominant-baseline: central; pointer-events: none; font-variant-numeric: tabular-nums; }
|
||||||
|
.anl-plan .anl-hell .anl-w { fill: #15181c; }
|
||||||
|
.anl-plan .anl-offline .anl-w { fill: #8d96a0; }
|
||||||
|
.anl-plan.anl-klein .anl-w { display: none; }
|
||||||
|
|
||||||
|
.anl-plan .anl-warnung { display: none; pointer-events: none; }
|
||||||
|
.anl-plan .anl-schwach .anl-warnung { display: inline; }
|
||||||
|
.anl-plan .anl-warnung circle { fill: #e5624f; stroke: #1c2024; stroke-width: 2; }
|
||||||
|
.anl-plan .anl-warnung .anl-warnung-hof { stroke: none; opacity: .5; transform-box: fill-box; transform-origin: center; animation: anl-puls 1.8s ease-out infinite; }
|
||||||
|
@keyframes anl-puls { from { transform: scale(.6); opacity: .6; } to { transform: scale(1.5); opacity: 0; } }
|
||||||
|
.anl-plan .anl-warnung text { fill: #fff; font-size: 13px; font-weight: 700; text-anchor: middle; dominant-baseline: central; }
|
||||||
|
|
||||||
|
.anl-plan .anl-schild { pointer-events: none; }
|
||||||
|
.anl-plan .anl-schild rect { fill: rgba(21, 24, 28, .86); stroke: rgba(255, 255, 255, .08); }
|
||||||
|
.anl-plan .anl-schild-wert { font-size: 23px; font-weight: 600; fill: var(--ton-schrift, #e8ede9); text-anchor: middle; dominant-baseline: central; font-variant-numeric: tabular-nums; }
|
||||||
|
.anl-plan .anl-schild-name { font-size: 11px; font-weight: 600; fill: var(--ton-schrift-2, #9aa6ad); text-anchor: middle; dominant-baseline: central; text-transform: uppercase; letter-spacing: .08em; }
|
||||||
|
.anl-plan .anl-nord path { fill: #808090; }
|
||||||
|
.anl-plan .anl-nord text { fill: #808090; font-size: 15px; font-weight: 600; text-anchor: middle; }
|
||||||
|
.anl-plan.anl-pause,
|
||||||
|
.anl-plan.anl-pause * { animation-play-state: paused !important; }
|
||||||
|
|
||||||
|
.anl .anl-legende { display: flex; flex-wrap: wrap; gap: .3rem 1.1rem; font-size: .78rem; color: var(--ton-schrift-2, #9aa6ad); padding: 0 .3rem; }
|
||||||
|
.anl .anl-legende i { display: inline-block; width: .75rem; height: .75rem; border-radius: 3px; margin-right: .35rem; vertical-align: -.1rem; }
|
||||||
|
.anl .anl-l-verlauf { width: 2.6rem !important; background: linear-gradient(90deg, #3b3a32, #f7c000); }
|
||||||
|
.anl .anl-l-warnung { background: #e5624f; border-radius: 50% !important; color: #fff; font-style: normal; font-size: .6rem; font-weight: 700; text-align: center; line-height: .75rem; }
|
||||||
|
.anl .anl-l-offline { background: #262b31; box-shadow: inset 0 0 0 1px #5c646d; }
|
||||||
|
|
||||||
|
.anl .anl-unten { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: .9rem; align-items: start; }
|
||||||
|
@media (max-width: 860px) { .anl .anl-unten { grid-template-columns: minmax(0, 1fr); } }
|
||||||
|
.anl .anl-fach { background: var(--ton-grund, #191d20); border-radius: .9rem; padding: .9rem 1rem; display: grid; gap: .7rem; align-content: start; }
|
||||||
|
.anl .anl-ueberschrift { font-size: .78rem; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: var(--ton-schrift-2, #9aa6ad); margin: 0; }
|
||||||
|
.anl .anl-kopf { display: flex; justify-content: space-between; align-items: center; gap: .6rem; }
|
||||||
|
.anl .anl-pille { font-size: .72rem; font-weight: 600; border-radius: 99px; padding: .1rem .55rem; white-space: nowrap; color: var(--ton-schrift-2, #9aa6ad); background: rgba(255, 255, 255, .06); }
|
||||||
|
.anl .anl-pille.anl-ok { color: #6cbe58; background: rgba(108, 190, 88, .13); }
|
||||||
|
.anl .anl-pille.anl-warn { color: #e8a33a; background: rgba(232, 163, 58, .14); }
|
||||||
|
.anl .anl-pille.anl-krit { color: #e5624f; background: rgba(229, 98, 79, .14); }
|
||||||
|
.anl .anl-gross { display: flex; flex-wrap: wrap; align-items: baseline; gap: .2rem .8rem; }
|
||||||
|
.anl .anl-gross b { font-size: 2rem; font-weight: 600; line-height: 1.1; font-variant-numeric: tabular-nums; color: var(--ton-schrift, #e8ede9); }
|
||||||
|
.anl .anl-gross span { color: var(--ton-schrift-2, #9aa6ad); font-size: .88rem; }
|
||||||
|
.anl .anl-funken { width: 100%; height: auto; display: block; }
|
||||||
|
.anl .anl-gitter { stroke: #2a3037; }
|
||||||
|
.anl .anl-funken-flaeche { fill: rgba(247, 192, 0, .14); }
|
||||||
|
.anl .anl-funken-linie { fill: none; stroke: #f7c000; stroke-width: 2; stroke-linejoin: round; }
|
||||||
|
.anl .anl-funken-punkt { fill: #f7c000; }
|
||||||
|
.anl .anl-achse { fill: var(--ton-schrift-3, #6f7a81); font-size: 10px; }
|
||||||
|
.anl .anl-funken-leer { color: var(--ton-schrift-3, #6f7a81); font-size: .82rem; padding: .7rem 0; }
|
||||||
|
.anl .anl-paare { display: grid; grid-template-columns: auto 1fr; gap: .25rem .9rem; font-size: .88rem; margin: 0; }
|
||||||
|
.anl .anl-paare dt { color: var(--ton-schrift-2, #9aa6ad); font-weight: 400; }
|
||||||
|
.anl .anl-paare dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }
|
||||||
|
.anl .anl-hinweis { font-size: .84rem; color: var(--ton-schrift-2, #9aa6ad); margin: 0; }
|
||||||
|
.anl button.anl-link { all: unset; cursor: pointer; color: #8fb8de; }
|
||||||
|
.anl button.anl-link:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
.anl .anl-wr,
|
||||||
|
.anl .anl-eingaenge,
|
||||||
|
.anl .anl-hinweise { list-style: none; margin: 0; padding: 0; display: grid; gap: .15rem; }
|
||||||
|
.anl .anl-wr button,
|
||||||
|
.anl .anl-eingaenge button,
|
||||||
|
.anl .anl-hinweise button { all: unset; box-sizing: border-box; cursor: pointer; width: 100%; border-radius: .5rem; padding: .42rem .55rem; transition: background .2s; }
|
||||||
|
.anl .anl-wr button:hover,
|
||||||
|
.anl .anl-eingaenge button:hover,
|
||||||
|
.anl .anl-hinweise button:hover { background: var(--ton-flaeche-2, #2b3237); }
|
||||||
|
.anl .anl-wr button:focus-visible,
|
||||||
|
.anl .anl-eingaenge button:focus-visible,
|
||||||
|
.anl .anl-hinweise button:focus-visible,
|
||||||
|
.anl button.anl-link:focus-visible { outline: 2px solid #8fb8de; outline-offset: 1px; }
|
||||||
|
.anl .anl-wr button { display: grid; grid-template-columns: minmax(0, 1fr) auto 6.5rem; gap: .7rem; align-items: center; font-size: .9rem; }
|
||||||
|
.anl .anl-wr button > .anl-pille { justify-self: end; }
|
||||||
|
.anl .anl-wr button[aria-pressed="true"] { background: var(--ton-flaeche-2, #2b3237); box-shadow: inset 3px 0 0 #f7c000; }
|
||||||
|
.anl .anl-wr-name small { display: block; color: var(--ton-schrift-3, #6f7a81); font-size: .74rem; }
|
||||||
|
.anl .anl-wr-wert { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
|
||||||
|
.anl .anl-wr-last { display: block; width: 4rem; height: 3px; margin: .25rem 0 0 auto; border-radius: 2px; background: #2b3139; overflow: hidden; }
|
||||||
|
.anl .anl-wr-last span { display: block; height: 100%; background: #f7c000; transition: width 1s; }
|
||||||
|
.anl .anl-eingaenge button { display: grid; grid-template-columns: 6.5rem minmax(0, 1fr) 4.2rem; gap: .6rem; align-items: center; font-size: .86rem; }
|
||||||
|
.anl .anl-eingaenge b { text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||||
|
.anl .anl-balken { height: 7px; border-radius: 4px; background: #2b3139; overflow: hidden; }
|
||||||
|
.anl .anl-balken span { display: block; height: 100%; border-radius: 4px; background: #f7c000; transition: width .8s cubic-bezier(.2, .7, .2, 1); }
|
||||||
|
.anl .anl-balken.anl-mittel span { background: #c98a22; }
|
||||||
|
.anl .anl-balken.anl-schwach span { background: #e5624f; }
|
||||||
|
.anl .anl-balken.anl-offline span,
|
||||||
|
.anl .anl-balken.anl-dunkel span { background: #5c646d; }
|
||||||
|
.anl .anl-hinweise button { display: flex; align-items: center; gap: .6rem; font-size: .88rem; }
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.anl *, .anl-plan * { animation: none !important; transition: none !important; }
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Karte: Umschalter Übersicht / Anlage -------------------------------- */
|
/* --- 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 { 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 { border: 0; color: var(--ton-schrift-2, #9aa6ad); font-weight: 600; padding: .2rem .8rem; }
|
||||||
|
|||||||
@@ -100,8 +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/energieflussKatalog.js",
|
"scripts" => ["js/solar/jahresstatistik.js", "js/solar/anlageKatalog.js", "js/solar/anlage.js",
|
||||||
"js/solar/energiefluss.js", "js/solar/solarMQTT.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,
|
||||||
@@ -114,8 +114,8 @@ if (isset($_GET["addUser"])) {
|
|||||||
"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/anlageKatalog.js", "js/solar/energieflussKatalog.js",
|
||||||
"js/solar/energieflussEinstellungen.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,
|
||||||
|
|||||||
@@ -0,0 +1,558 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
Anlage-Ansicht: die PV-Anlage als Lageplan mit Leistung je Platte.
|
||||||
|
|
||||||
|
const plan = new Anlage(behaelter, { katalog: ANLAGE_KATALOG });
|
||||||
|
plan.zeichnen();
|
||||||
|
plan.aktualisieren(mqttData);
|
||||||
|
|
||||||
|
Was die Anlage ausmacht (Flächen, Wechselrichter, Lage jeder Platte),
|
||||||
|
steht in js/solar/anlageKatalog.js. Hier steht nur, wie ein Plan
|
||||||
|
grundsätzlich aussieht und wie aus den Werten ein Zustand wird - im
|
||||||
|
selben Stil wie die Energiefluss-Übersicht (energiefluss.js).
|
||||||
|
|
||||||
|
Farbe einer Platte: ihre Leistung im Vergleich zum Median ihrer Fläche.
|
||||||
|
So fällt eine verschattete oder defekte Platte auf, ohne dass Sonnenstand
|
||||||
|
oder Bewölkung eine Rolle spielen. Bei schwachem Licht (Median unter
|
||||||
|
SCHWACHLICHT) wird nicht verglichen - dann liefern alle fast nichts.
|
||||||
|
Liefert ein Wechselrichter keine Daten, sind seine Platten "ohne Daten"
|
||||||
|
und nicht "auffällig".
|
||||||
|
|
||||||
|
Den Verlauf je Platte gibt es in der Datenbank nicht; er wird hier seit
|
||||||
|
dem Aufruf der Seite mitgeschrieben.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const NS = "http://www.w3.org/2000/svg";
|
||||||
|
const HOCHKANT_UNTER = 720;
|
||||||
|
const SCHWACHLICHT = 30; // W Median, darunter kein Vergleich
|
||||||
|
const VERLAUF_ABSTAND = 15000; // ms zwischen zwei Punkten
|
||||||
|
const VERLAUF_DAUER = 40 * 60000; // ms, die der Verlauf zurückreicht
|
||||||
|
|
||||||
|
let laufendeNummer = 0;
|
||||||
|
|
||||||
|
const kw = w => (Math.abs(w) / 1000).toFixed(Math.abs(w) < 10000 ? 2 : 1).replace(".", ",") + " kW";
|
||||||
|
const watt = w => Math.abs(w) < 1000 ? Math.round(w) + " W" : kw(w);
|
||||||
|
const schutz = s => String(s).replace(/[&<>"]/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Füllfarbe einer Platte: von gedämpftem Oliv (nichts) zur Sonnenfarbe
|
||||||
|
* (so viel wie die stärksten). Als --fuellung, damit CSS sie weich
|
||||||
|
* überblendet und "ohne Daten" sie überstimmen kann.
|
||||||
|
*/
|
||||||
|
const DUNKEL = [59, 58, 50];
|
||||||
|
function setzeFuellung(g, anteil, farbe) {
|
||||||
|
const ziel = [1, 3, 5].map(i => parseInt(farbe.substr(i, 2), 16));
|
||||||
|
const t = Math.pow(Math.max(0, Math.min(1, anteil)), 0.8);
|
||||||
|
const rgb = "rgb(" + DUNKEL.map((d, i) => Math.round(d + (ziel[i] - d) * t)).join(",") + ")";
|
||||||
|
if (g._fuellung !== rgb) { g._fuellung = rgb; g.style.setProperty("--fuellung", rgb); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function median(werte) {
|
||||||
|
if (!werte.length) return 0;
|
||||||
|
const s = [...werte].sort((a, b) => a - b);
|
||||||
|
return s[Math.floor(s.length / 2)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function leser(daten) {
|
||||||
|
return {
|
||||||
|
zahl(t) { const v = Number(String(daten[t])); return isFinite(v) ? v : 0; },
|
||||||
|
text(t) { return daten[t] === undefined ? undefined : String(daten[t]); },
|
||||||
|
json(t) { try { return JSON.parse(String(daten[t])); } catch (e) { return null; } },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class Anlage {
|
||||||
|
constructor(behaelter, opt = {}) {
|
||||||
|
this.behaelter = behaelter;
|
||||||
|
this.katalog = opt.katalog || ANLAGE_KATALOG;
|
||||||
|
this.nummer = ++laufendeNummer;
|
||||||
|
this.auswahl = null; // {art: "platte"|"string"|"wr", schluessel}
|
||||||
|
this.verlauf = {}; // Eingang -> [[zeit, W], ...]
|
||||||
|
this.letzterPunkt = 0;
|
||||||
|
this.stand = null;
|
||||||
|
|
||||||
|
const k = this.katalog;
|
||||||
|
this.flaeche = Object.fromEntries(k.flaechen.map(f => [f.id, f]));
|
||||||
|
this.wrNachEingang = {};
|
||||||
|
k.wechselrichter.forEach(w => w.eingaenge.forEach((e, i) => { this.wrNachEingang[e] = { wr: w, index: i }; }));
|
||||||
|
|
||||||
|
this.beobachter = new ResizeObserver(() => {
|
||||||
|
if (!this.behaelter.clientWidth) return;
|
||||||
|
if ((this.behaelter.clientWidth < HOCHKANT_UNTER) !== this.hochkant) this.zeichnen();
|
||||||
|
else this.masstab();
|
||||||
|
});
|
||||||
|
this.beobachter.observe(behaelter);
|
||||||
|
this.sichtbar = true;
|
||||||
|
new IntersectionObserver(e => { this.sichtbar = e[0].isIntersecting; this.pausieren(); }).observe(behaelter);
|
||||||
|
document.addEventListener("visibilitychange", () => this.pausieren());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------- Geometrie */
|
||||||
|
/** Hochkant: um 90 Grad im Uhrzeigersinn gedreht. Norden dreht mit. */
|
||||||
|
rechteck(x, y, w, h) {
|
||||||
|
return this.hochkant ? [-(y + h), x, h, w] : [x, y, w, h];
|
||||||
|
}
|
||||||
|
punkt(x, y) {
|
||||||
|
return this.hochkant ? [-y, x] : [x, y];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------------------------------------------- Zeichnen */
|
||||||
|
zeichnen() {
|
||||||
|
const breite = this.behaelter.clientWidth;
|
||||||
|
this.hochkant = breite > 0 && breite < HOCHKANT_UNTER;
|
||||||
|
const id = "anl" + this.nummer;
|
||||||
|
const k = this.katalog;
|
||||||
|
|
||||||
|
this.behaelter.innerHTML = `
|
||||||
|
<div class="anl">
|
||||||
|
<div class="anl-flaechen"></div>
|
||||||
|
<div class="anl-plan-rahmen">
|
||||||
|
<svg class="anl-plan${this.hochkant ? " anl-hoch" : ""}" role="img" aria-label="Lageplan der PV-Anlage"></svg>
|
||||||
|
</div>
|
||||||
|
<div class="anl-legende">
|
||||||
|
<span><i class="anl-l-verlauf"></i>Leistung im Vergleich zu den stärksten Platten der Fläche</span>
|
||||||
|
<span><i class="anl-l-warnung">!</i>deutlich unter dem Mittel der Fläche</span>
|
||||||
|
<span><i class="anl-l-offline"></i>ohne Daten</span>
|
||||||
|
</div>
|
||||||
|
<div class="anl-unten">
|
||||||
|
<div class="anl-fach anl-auswahl" aria-live="polite"></div>
|
||||||
|
<div class="anl-fach">
|
||||||
|
<h3 class="anl-ueberschrift">Wechselrichter</h3>
|
||||||
|
<ul class="anl-wr"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
this.dom = {
|
||||||
|
flaechen: this.behaelter.querySelector(".anl-flaechen"),
|
||||||
|
svg: this.behaelter.querySelector(".anl-plan"),
|
||||||
|
auswahl: this.behaelter.querySelector(".anl-auswahl"),
|
||||||
|
wr: this.behaelter.querySelector(".anl-wr"),
|
||||||
|
};
|
||||||
|
const svg = this.dom.svg;
|
||||||
|
|
||||||
|
el("pattern", { id: id + "-zellen", width: 10, height: 10, patternUnits: "userSpaceOnUse" }, el("defs", null, svg))
|
||||||
|
.appendChild(el("path", { d: "M10 0V10M0 10H10", fill: "none", stroke: "rgba(0,0,0,.2)", "stroke-width": 1 }));
|
||||||
|
|
||||||
|
// Flächen
|
||||||
|
const umrisse = el("g", { class: "anl-umrisse" }, svg);
|
||||||
|
for (const f of k.flaechen) {
|
||||||
|
const g = el("g", { style: `--farbe:${f.farbe}`, "data-flaeche": f.id }, umrisse);
|
||||||
|
if (Array.isArray(f.umriss)) {
|
||||||
|
f.umriss.forEach(p => el("polygon", {
|
||||||
|
class: "anl-dachflaeche",
|
||||||
|
points: p.split(" ").map(xy => this.punkt(...xy.split(",").map(Number)).join(",")).join(" "),
|
||||||
|
}, g));
|
||||||
|
} else if (f.umriss) {
|
||||||
|
const [x, y, w, h] = this.rechteck(f.umriss.x, f.umriss.y, f.umriss.w, f.umriss.h);
|
||||||
|
el("rect", { class: "anl-bereich", x, y, width: w, height: h, rx: 12 }, g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strings (ohne Einzelwerte)
|
||||||
|
this.stringKnoten = {};
|
||||||
|
const strings = el("g", { class: "anl-platten" }, svg);
|
||||||
|
for (const s of k.strings) {
|
||||||
|
const g = el("g", { class: "anl-string anl-dunkel", "data-string": s.id, style: `--farbe:${s.farbe}` }, strings);
|
||||||
|
s.platten.forEach(r => this.platteZeichnen(g, id, r[0], r[1], r[2], r[3]));
|
||||||
|
this.stringKnoten[s.id] = g;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Platten
|
||||||
|
this.platteKnoten = {};
|
||||||
|
for (const p of k.platten) {
|
||||||
|
const g = el("g", { class: "anl-platte anl-dunkel", "data-eingang": p.eingang }, strings);
|
||||||
|
const r = this.platteZeichnen(g, id, p.x, p.y, p.w || 70, p.h || 40);
|
||||||
|
const t = el("text", { class: "anl-w", x: r[0] + r[2] / 2, y: r[1] + r[3] / 2 }, g);
|
||||||
|
t.textContent = "–";
|
||||||
|
const warn = el("g", { class: "anl-warnung", transform: `translate(${r[0] + r[2] - 3} ${r[1] + 3})` }, g);
|
||||||
|
el("circle", { r: 13, class: "anl-warnung-hof" }, warn);
|
||||||
|
el("circle", { r: 8.5 }, warn);
|
||||||
|
el("text", { y: 0.5 }, warn).textContent = "!";
|
||||||
|
this.platteKnoten[p.eingang] = { g, t };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stringwerte als Schild
|
||||||
|
this.schilder = {};
|
||||||
|
for (const s of k.strings) {
|
||||||
|
const [x, y] = this.punkt(...s.schild);
|
||||||
|
const g = el("g", { class: "anl-schild", transform: `translate(${x} ${y})` }, svg);
|
||||||
|
el("rect", { x: -64, y: -26, width: 128, height: 52, rx: 12 }, g);
|
||||||
|
const w = el("text", { class: "anl-schild-wert", y: -5 }, g);
|
||||||
|
const n = el("text", { class: "anl-schild-name", y: 15 }, g);
|
||||||
|
n.textContent = s.name;
|
||||||
|
this.schilder[s.id] = w;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nordpfeil
|
||||||
|
const nord = el("g", { class: "anl-nord" }, svg);
|
||||||
|
const [nx, ny] = this.punkt(k.nord.x, k.nord.y);
|
||||||
|
nord.setAttribute("transform", `translate(${nx} ${ny}) rotate(${k.nord.winkel + (this.hochkant ? 90 : 0)})`);
|
||||||
|
el("path", { d: "M0-26L12 14 0 7-12 14Z" }, nord);
|
||||||
|
const nt = el("text", { y: 34 }, nord);
|
||||||
|
nt.textContent = "N";
|
||||||
|
if (this.hochkant) nt.setAttribute("transform", "rotate(-100 0 34)");
|
||||||
|
|
||||||
|
this.rahmen();
|
||||||
|
this.masstab();
|
||||||
|
this.binden();
|
||||||
|
this.pausieren();
|
||||||
|
if (this.stand) this.anzeigen();
|
||||||
|
}
|
||||||
|
|
||||||
|
platteZeichnen(g, id, x, y, w, h) {
|
||||||
|
const r = this.rechteck(x, y, w, h);
|
||||||
|
// --d: Verzögerung des Auftritts, --glanz: Takt der Lichtwelle - beide
|
||||||
|
// laufen über die Anlage von links oben nach rechts unten.
|
||||||
|
const teil = el("g", { class: "anl-teil", style: `--d:${Math.round((x + y) / 2.2)};--glanz:${((x + y * 0.4) / 1500).toFixed(3)}` }, g);
|
||||||
|
el("rect", { class: "anl-koerper", x: r[0], y: r[1], width: r[2], height: r[3], rx: 4 }, teil);
|
||||||
|
el("rect", { class: "anl-zellen", x: r[0], y: r[1], width: r[2], height: r[3], rx: 4, fill: `url(#${id}-zellen)` }, teil);
|
||||||
|
el("rect", { class: "anl-glanz", x: r[0], y: r[1], width: r[2], height: r[3], rx: 4 }, teil);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Zeichenfläche aus dem Katalog - getBBox() taugt nicht, die Ansicht ist beim Zeichnen oft verborgen. */
|
||||||
|
rahmen() {
|
||||||
|
const k = this.katalog;
|
||||||
|
const punkte = [];
|
||||||
|
const rechteck = (x, y, w, h) => { const r = this.rechteck(x, y, w, h); punkte.push([r[0], r[1]], [r[0] + r[2], r[1] + r[3]]); };
|
||||||
|
k.platten.forEach(p => rechteck(p.x, p.y, p.w || 70, p.h || 40));
|
||||||
|
k.strings.forEach(s => s.platten.forEach(r => rechteck(...r)));
|
||||||
|
k.flaechen.forEach(f => {
|
||||||
|
if (Array.isArray(f.umriss)) f.umriss.forEach(p => p.split(" ").forEach(xy => punkte.push(this.punkt(...xy.split(",").map(Number)))));
|
||||||
|
else if (f.umriss) rechteck(f.umriss.x, f.umriss.y, f.umriss.w, f.umriss.h);
|
||||||
|
});
|
||||||
|
const [nx, ny] = this.punkt(k.nord.x, k.nord.y);
|
||||||
|
punkte.push([nx - 30, ny - 40], [nx + 30, ny + 45]);
|
||||||
|
const xs = punkte.map(p => p[0]), ys = punkte.map(p => p[1]);
|
||||||
|
const rand = 24;
|
||||||
|
const x0 = Math.min(...xs) - rand, y0 = Math.min(...ys) - rand;
|
||||||
|
this.dom.svg.setAttribute("viewBox",
|
||||||
|
[x0, y0, Math.max(...xs) + rand - x0, Math.max(...ys) + rand - y0].map(Math.round).join(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Beschriftung ausblenden, wenn sie zu klein würde. */
|
||||||
|
masstab() {
|
||||||
|
const vb = this.dom && this.dom.svg.viewBox.baseVal;
|
||||||
|
if (!vb || !vb.width) return;
|
||||||
|
const pxJeEinheit = this.dom.svg.clientWidth / vb.width;
|
||||||
|
this.dom.svg.classList.toggle("anl-klein", pxJeEinheit < 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
binden() {
|
||||||
|
this.dom.svg.addEventListener("click", ev => {
|
||||||
|
const p = ev.target.closest("[data-eingang]");
|
||||||
|
const s = ev.target.closest("[data-string]");
|
||||||
|
if (p) this.waehle("platte", Number(p.dataset.eingang));
|
||||||
|
else if (s) this.waehle("string", s.dataset.string);
|
||||||
|
else this.waehle(null);
|
||||||
|
});
|
||||||
|
const zuWahl = ev => {
|
||||||
|
const b = ev.target.closest("[data-wahl]");
|
||||||
|
if (!b) return;
|
||||||
|
const [art, schluessel] = b.dataset.wahl.split(":");
|
||||||
|
this.waehle(art || null, art === "string" ? schluessel : Number(schluessel));
|
||||||
|
};
|
||||||
|
this.dom.wr.addEventListener("click", zuWahl);
|
||||||
|
this.dom.auswahl.addEventListener("click", zuWahl);
|
||||||
|
}
|
||||||
|
|
||||||
|
waehle(art, schluessel) {
|
||||||
|
const gleich = this.auswahl && this.auswahl.art === art && this.auswahl.schluessel === schluessel;
|
||||||
|
this.auswahl = art && !gleich ? { art, schluessel } : null;
|
||||||
|
this.detailSig = null;
|
||||||
|
if (this.stand) this.anzeigen();
|
||||||
|
}
|
||||||
|
|
||||||
|
pausieren() {
|
||||||
|
if (this.dom) this.dom.svg.classList.toggle("anl-pause", !this.sichtbar || document.hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------- Aktualisieren */
|
||||||
|
aktualisieren(daten) {
|
||||||
|
const m = leser(daten);
|
||||||
|
const pvn = m.json("solarManager/P_PVn") || [];
|
||||||
|
const k = this.katalog;
|
||||||
|
|
||||||
|
const wr = {};
|
||||||
|
for (const w of k.wechselrichter) {
|
||||||
|
const b = "solarManager/inverters" + w.nr + "/";
|
||||||
|
const geschaetzt = m.text(b + "estimated");
|
||||||
|
wr[w.nr] = {
|
||||||
|
def: w,
|
||||||
|
ac: m.zahl(b + "p_AC"),
|
||||||
|
temp: m.zahl(b + "temp"),
|
||||||
|
gestoert: m.zahl(b + "error") > 0,
|
||||||
|
geschaetzt: geschaetzt === "True" || Number(geschaetzt) > 0,
|
||||||
|
limit: m.zahl(b + "limit"),
|
||||||
|
gemeldet: m.text(b + "name") !== undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const platten = k.platten.map(p => {
|
||||||
|
const zu = this.wrNachEingang[p.eingang];
|
||||||
|
const w = Math.max(0, Number(pvn[p.eingang]) || 0);
|
||||||
|
return { def: p, w, wr: zu ? wr[zu.wr.nr] : null, index: zu ? zu.index : 0, offline: zu ? wr[zu.wr.nr].gestoert : false };
|
||||||
|
});
|
||||||
|
// Zwei getrennte Fragen:
|
||||||
|
// hell - wie viel liefert die Platte im Vergleich zu den stärksten
|
||||||
|
// ihrer Fläche (90. Perzentil)? Nur Farbe, kein Urteil.
|
||||||
|
// klasse - fällt sie auf? Nur bei genug Licht, gemessen am Median.
|
||||||
|
// Früher bestimmte der Median beides; auf der Veranda mit Platten in
|
||||||
|
// verschiedenen Richtungen waren dann nachmittags alle "dunkel", auch
|
||||||
|
// die mit 200 W.
|
||||||
|
const mittel = {}, bezug = {};
|
||||||
|
for (const f of k.flaechen) {
|
||||||
|
const werte = platten.filter(p => p.def.flaeche === f.id && !p.offline).map(p => p.w).sort((a, b) => a - b);
|
||||||
|
mittel[f.id] = median(werte);
|
||||||
|
bezug[f.id] = werte.length ? werte[Math.floor(0.9 * (werte.length - 1))] : 0;
|
||||||
|
}
|
||||||
|
for (const p of platten) {
|
||||||
|
const md = mittel[p.def.flaeche];
|
||||||
|
p.anteil = md > 0 ? p.w / md : 0;
|
||||||
|
p.hell = bezug[p.def.flaeche] > 0 ? Math.min(1, p.w / bezug[p.def.flaeche]) : 0;
|
||||||
|
p.klasse = p.offline ? "offline" : md < SCHWACHLICHT ? "dunkel"
|
||||||
|
: p.anteil >= 0.9 ? "gut" : p.anteil >= 0.7 ? "mittel" : "schwach";
|
||||||
|
}
|
||||||
|
const strings = k.strings.map(s => {
|
||||||
|
const w = Math.max(0, Number(pvn[s.eingang]) || 0);
|
||||||
|
return { def: s, w, jePlatte: w / s.platten.length };
|
||||||
|
});
|
||||||
|
for (const s of strings) {
|
||||||
|
const max = Math.max(...strings.filter(x => x.def.flaeche === s.def.flaeche).map(x => x.jePlatte));
|
||||||
|
s.hell = max > 0 ? s.jePlatte / max : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verlauf mitschreiben
|
||||||
|
const jetzt = Date.now();
|
||||||
|
if (jetzt - this.letzterPunkt >= VERLAUF_ABSTAND) {
|
||||||
|
this.letzterPunkt = jetzt;
|
||||||
|
pvn.forEach((w, i) => {
|
||||||
|
const v = this.verlauf[i] || (this.verlauf[i] = []);
|
||||||
|
v.push([jetzt, Number(w) || 0]);
|
||||||
|
while (v.length && jetzt - v[0][0] > VERLAUF_DAUER) v.shift();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stand = { platten, strings, wr, mittel, pvn, gesamt: m.zahl("solarManager/P_PV") };
|
||||||
|
if (this.dom) this.anzeigen();
|
||||||
|
}
|
||||||
|
|
||||||
|
anzeigen() {
|
||||||
|
const { platten, strings, wr, gesamt } = this.stand;
|
||||||
|
const k = this.katalog;
|
||||||
|
const wahl = this.auswahl;
|
||||||
|
const wahlWr = wahl && wahl.art === "wr" ? wahl.schluessel
|
||||||
|
: wahl && wahl.art === "platte" ? (this.wrNachEingang[wahl.schluessel] || {}).wr?.nr
|
||||||
|
: wahl && wahl.art === "string" ? 0 : null;
|
||||||
|
|
||||||
|
// Platten
|
||||||
|
for (const p of platten) {
|
||||||
|
const n = this.platteKnoten[p.def.eingang];
|
||||||
|
if (!n) continue;
|
||||||
|
const klasse = "anl-platte anl-" + p.klasse + (p.hell > 0.45 ? " anl-hell" : "")
|
||||||
|
+ (wahl && wahl.art === "platte" && wahl.schluessel === p.def.eingang ? " anl-gewaehlt"
|
||||||
|
: wahlWr !== null && wahlWr !== undefined && p.wr && p.wr.def.nr === wahlWr ? " anl-geschwister" : "");
|
||||||
|
if (n.g.getAttribute("class") !== klasse) n.g.setAttribute("class", klasse);
|
||||||
|
setzeFuellung(n.g, p.hell, "#f7c000");
|
||||||
|
const text = p.offline ? "–" : String(Math.round(p.w));
|
||||||
|
if (n.t.textContent !== text) n.t.textContent = text;
|
||||||
|
}
|
||||||
|
for (const s of strings) {
|
||||||
|
const g = this.stringKnoten[s.def.id];
|
||||||
|
const klasse = "anl-string" + (s.hell > 0.45 ? " anl-hell" : "")
|
||||||
|
+ (wahl && wahl.art === "string" && wahl.schluessel === s.def.id ? " anl-gewaehlt"
|
||||||
|
: wahlWr === 0 ? " anl-geschwister" : "");
|
||||||
|
if (g.getAttribute("class") !== klasse) g.setAttribute("class", klasse);
|
||||||
|
setzeFuellung(g, s.w < 5 ? 0 : s.hell, s.def.farbe);
|
||||||
|
const text = kw(s.w);
|
||||||
|
if (this.schilder[s.def.id].textContent !== text) this.schilder[s.def.id].textContent = text;
|
||||||
|
}
|
||||||
|
this.dom.svg.classList.toggle("anl-sonnig", gesamt > 300);
|
||||||
|
|
||||||
|
// Flächen oben
|
||||||
|
const summen = k.flaechen.map(f => {
|
||||||
|
const w = platten.filter(p => p.def.flaeche === f.id).reduce((a, p) => a + p.w, 0)
|
||||||
|
+ strings.filter(s => s.def.flaeche === f.id).reduce((a, s) => a + s.w, 0);
|
||||||
|
const n = platten.filter(p => p.def.flaeche === f.id).length
|
||||||
|
+ k.strings.filter(s => s.flaeche === f.id).reduce((a, s) => a + s.platten.length, 0);
|
||||||
|
return { f, w, n };
|
||||||
|
});
|
||||||
|
const summe = summen.reduce((a, s) => a + s.w, 0) || 1;
|
||||||
|
const flaechenHtml = summen.map(s => `
|
||||||
|
<div class="anl-flaeche" style="--farbe:${s.f.farbe}">
|
||||||
|
<div class="anl-flaeche-kopf"><i></i><span>${schutz(s.f.name)}</span><b>${kw(s.w)}</b></div>
|
||||||
|
<div class="anl-flaeche-balken"><span style="width:${(s.w / summe * 100).toFixed(1)}%"></span></div>
|
||||||
|
<small>${s.n} Platten · ${Math.round(s.w / summe * 100)} % der Erzeugung</small>
|
||||||
|
</div>`).join("");
|
||||||
|
if (this.dom.flaechen._html !== flaechenHtml) {
|
||||||
|
// Beim ersten Mal ganz schreiben, danach nur Werte - sonst springen die Balken.
|
||||||
|
if (!this.dom.flaechen.children.length) {
|
||||||
|
this.dom.flaechen.innerHTML = flaechenHtml;
|
||||||
|
} else {
|
||||||
|
[...this.dom.flaechen.children].forEach((c, i) => {
|
||||||
|
const s = summen[i];
|
||||||
|
c.querySelector("b").textContent = kw(s.w);
|
||||||
|
c.querySelector(".anl-flaeche-balken span").style.width = (s.w / summe * 100).toFixed(1) + "%";
|
||||||
|
c.querySelector("small").textContent = s.n + " Platten · " + Math.round(s.w / summe * 100) + " % der Erzeugung";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.dom.flaechen._html = flaechenHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wechselrichter
|
||||||
|
const wrHtml = k.wechselrichter.map(d => {
|
||||||
|
const w = wr[d.nr];
|
||||||
|
const pille = !w.gemeldet || w.gestoert ? '<span class="anl-pille anl-krit">keine Daten</span>'
|
||||||
|
: w.geschaetzt ? '<span class="anl-pille anl-warn">hochgerechnet</span>'
|
||||||
|
: '<span class="anl-pille anl-ok">ok</span>';
|
||||||
|
const ausl = w.limit > 0 ? Math.min(100, w.ac / w.limit * 100) : 0;
|
||||||
|
const aktiv = wahlWr === d.nr ? ' aria-pressed="true"' : ' aria-pressed="false"';
|
||||||
|
return `<li><button type="button" data-wahl="wr:${d.nr}"${aktiv}>
|
||||||
|
<span class="anl-wr-name">${schutz(d.name)}<small>${schutz(d.modell)}${w.temp > 0 && !w.gestoert ? " · " + Math.round(w.temp) + " °C" : ""}</small></span>
|
||||||
|
<span class="anl-wr-wert">${w.gestoert ? "–" : watt(w.ac)}${w.limit > 0 ? `<span class="anl-wr-last"><span style="width:${ausl.toFixed(0)}%"></span></span>` : ""}</span>
|
||||||
|
${pille}
|
||||||
|
</button></li>`;
|
||||||
|
}).join("");
|
||||||
|
if (this.dom.wr._html !== wrHtml) { this.dom.wr.innerHTML = wrHtml; this.dom.wr._html = wrHtml; }
|
||||||
|
|
||||||
|
// Auswahl
|
||||||
|
const detail = this.detailHtml();
|
||||||
|
if (this.dom.auswahl._html !== detail) { this.dom.auswahl.innerHTML = detail; this.dom.auswahl._html = detail; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------ Detail */
|
||||||
|
plattenName(p) {
|
||||||
|
const f = this.flaeche[p.def.flaeche];
|
||||||
|
return p.def.nr ? `${f.name} · Platte #${p.def.nr}` : `${p.wr ? p.wr.def.name : f.name} · Eingang ${p.index + 1}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
detailHtml() {
|
||||||
|
const { platten, strings, wr, mittel } = this.stand;
|
||||||
|
const wahl = this.auswahl;
|
||||||
|
|
||||||
|
if (wahl && wahl.art === "platte") {
|
||||||
|
const p = platten.find(x => x.def.eingang === wahl.schluessel);
|
||||||
|
if (p) {
|
||||||
|
const f = this.flaeche[p.def.flaeche];
|
||||||
|
const pille = p.offline ? '<span class="anl-pille anl-krit">keine Daten</span>'
|
||||||
|
: p.klasse === "schwach" ? '<span class="anl-pille anl-krit">auffällig</span>'
|
||||||
|
: p.klasse === "mittel" ? '<span class="anl-pille anl-warn">etwas schwächer</span>'
|
||||||
|
: p.klasse === "dunkel" ? '<span class="anl-pille">wenig Licht</span>'
|
||||||
|
: '<span class="anl-pille anl-ok">normal</span>';
|
||||||
|
const vergleich = p.offline ? "Wechselrichter meldet nichts"
|
||||||
|
: mittel[p.def.flaeche] < SCHWACHLICHT ? "zu wenig Licht zum Vergleichen"
|
||||||
|
: Math.round(p.anteil * 100) + " % des Mittels " + f.name;
|
||||||
|
return `
|
||||||
|
<div class="anl-kopf"><h3 class="anl-ueberschrift">${schutz(this.plattenName(p))}</h3>${pille}</div>
|
||||||
|
<div class="anl-gross"><b>${p.offline ? "–" : watt(p.w)}</b><span>${vergleich}</span></div>
|
||||||
|
${this.funkenlinie(p.def.eingang)}
|
||||||
|
<dl class="anl-paare">
|
||||||
|
<dt>Wechselrichter</dt><dd><button type="button" class="anl-link" data-wahl="wr:${p.wr.def.nr}">${schutz(p.wr.def.name)}</button></dd>
|
||||||
|
<dt>Eingang</dt><dd>${p.index + 1} von ${p.wr.def.eingaenge.length}</dd>
|
||||||
|
<dt>Mittel ${schutz(f.name)}</dt><dd>${watt(mittel[p.def.flaeche])}</dd>
|
||||||
|
<dt>Temperatur Wechselrichter</dt><dd>${p.wr.temp > 0 && !p.offline ? Math.round(p.wr.temp) + " °C" : "–"}</dd>
|
||||||
|
</dl>
|
||||||
|
${p.klasse === "schwach" ? '<p class="anl-hinweis">Liefert deutlich weniger als die anderen Platten dieser Fläche. Mögliche Gründe: Verschattung, Schmutz, ein loser Stecker – oder die Platte liegt anders ausgerichtet.</p>' : ""}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wahl && wahl.art === "string") {
|
||||||
|
const s = strings.find(x => x.def.id === wahl.schluessel);
|
||||||
|
if (s) {
|
||||||
|
return `
|
||||||
|
<div class="anl-kopf"><h3 class="anl-ueberschrift">${schutz(this.flaeche[s.def.flaeche].name)} · ${schutz(s.def.name)}</h3><span class="anl-pille">Wert je String</span></div>
|
||||||
|
<div class="anl-gross"><b>${kw(s.w)}</b><span>${s.def.platten.length} Platten in Reihe</span></div>
|
||||||
|
${this.funkenlinie(s.def.eingang)}
|
||||||
|
<dl class="anl-paare">
|
||||||
|
<dt>Wechselrichter</dt><dd><button type="button" class="anl-link" data-wahl="wr:0">${schutz(wr[0].def.name)}</button></dd>
|
||||||
|
<dt>je Platte rechnerisch</dt><dd>${watt(s.w / s.def.platten.length)}</dd>
|
||||||
|
</dl>
|
||||||
|
<p class="anl-hinweis">Die Platten hängen in Reihe am Wechselrichter. Er misst nur den ganzen String – eine einzelne schwache Platte lässt sich hier nicht erkennen.</p>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wahl && wahl.art === "wr") {
|
||||||
|
const w = wr[wahl.schluessel];
|
||||||
|
if (w) {
|
||||||
|
const werte = w.def.eingaenge.map(e => Math.max(0, Number(this.stand.pvn[e]) || 0));
|
||||||
|
const max = Math.max(1, ...werte);
|
||||||
|
const eingaenge = w.def.eingaenge.map((e, i) => {
|
||||||
|
const p = platten.find(x => x.def.eingang === e);
|
||||||
|
const s = strings.find(x => x.def.eingang === e);
|
||||||
|
const name = p ? (p.def.nr ? "Platte #" + p.def.nr : "Eingang " + (i + 1)) : s ? s.def.name : "Eingang " + (i + 1);
|
||||||
|
const wahlZiel = p ? "platte:" + e : s ? "string:" + s.def.id : "";
|
||||||
|
return `<li><button type="button" data-wahl="${wahlZiel}">
|
||||||
|
<span>${schutz(name)}</span>
|
||||||
|
<span class="anl-balken anl-${p ? p.klasse : "gut"}"><span style="width:${(werte[i] / max * 100).toFixed(1)}%"></span></span>
|
||||||
|
<b>${w.gestoert ? "–" : watt(werte[i])}</b>
|
||||||
|
</button></li>`;
|
||||||
|
}).join("");
|
||||||
|
const pille = w.gestoert ? '<span class="anl-pille anl-krit">keine Daten</span>'
|
||||||
|
: w.geschaetzt ? '<span class="anl-pille anl-warn">hochgerechnet</span>' : '<span class="anl-pille anl-ok">ok</span>';
|
||||||
|
return `
|
||||||
|
<div class="anl-kopf"><h3 class="anl-ueberschrift">${schutz(w.def.name)}</h3>${pille}</div>
|
||||||
|
<div class="anl-gross"><b>${w.gestoert ? "–" : watt(w.ac)}</b><span>${schutz(w.def.modell)}</span></div>
|
||||||
|
<ul class="anl-eingaenge">${eingaenge}</ul>
|
||||||
|
<dl class="anl-paare">
|
||||||
|
${w.limit > 0 ? `<dt>Leistungsgrenze</dt><dd>${watt(w.limit)} · ${Math.round(w.ac / w.limit * 100)} % genutzt</dd>` : ""}
|
||||||
|
<dt>Temperatur</dt><dd>${w.temp > 0 && !w.gestoert ? Math.round(w.temp) + " °C" : "–"}</dd>
|
||||||
|
</dl>
|
||||||
|
${w.gestoert ? '<p class="anl-hinweis">Der Wechselrichter antwortet der DTU gerade nicht. Seine Platten stehen deshalb auf „ohne Daten“ – über ihren Zustand sagt das nichts.</p>' : ""}
|
||||||
|
${w.geschaetzt ? '<p class="anl-hinweis">Aktuelle Werte fehlen; die Leistung ist aus den anderen Wechselrichtern hochgerechnet.</p>' : ""}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ohne Auswahl: Zusammenfassung mit allem, was Aufmerksamkeit braucht.
|
||||||
|
const auffaellig = platten.filter(p => p.klasse === "schwach");
|
||||||
|
const ohne = Object.values(wr).filter(w => w.gestoert || !w.gemeldet);
|
||||||
|
const geschaetzt = Object.values(wr).filter(w => w.geschaetzt && !w.gestoert);
|
||||||
|
const n = platten.length + this.katalog.strings.reduce((a, s) => a + s.platten.length, 0);
|
||||||
|
const punkte = [
|
||||||
|
...ohne.map(w => `<li><button type="button" data-wahl="wr:${w.def.nr}"><span class="anl-pille anl-krit">keine Daten</span>${schutz(w.def.name)}</button></li>`),
|
||||||
|
...geschaetzt.map(w => `<li><button type="button" data-wahl="wr:${w.def.nr}"><span class="anl-pille anl-warn">hochgerechnet</span>${schutz(w.def.name)}</button></li>`),
|
||||||
|
...auffaellig.map(p => `<li><button type="button" data-wahl="platte:${p.def.eingang}"><span class="anl-pille anl-krit">${Math.round(p.anteil * 100)} %</span>${schutz(this.plattenName(p))}</button></li>`),
|
||||||
|
];
|
||||||
|
return `
|
||||||
|
<div class="anl-kopf"><h3 class="anl-ueberschrift">Anlage</h3><span class="anl-pille ${punkte.length ? "anl-warn" : "anl-ok"}">${punkte.length ? punkte.length + " Hinweis" + (punkte.length > 1 ? "e" : "") : "alles in Ordnung"}</span></div>
|
||||||
|
<div class="anl-gross"><b>${kw(this.stand.gesamt)}</b><span>${n} Platten an ${this.katalog.wechselrichter.length} Wechselrichtern</span></div>
|
||||||
|
${punkte.length ? `<ul class="anl-hinweise">${punkte.join("")}</ul>` : ""}
|
||||||
|
<p class="anl-hinweis">Eine Platte oder einen Wechselrichter antippen für Einzelheiten.</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
funkenlinie(eingang) {
|
||||||
|
const v = this.verlauf[eingang] || [];
|
||||||
|
if (v.length < 3) {
|
||||||
|
return '<div class="anl-funken-leer">Der Verlauf wird ab jetzt mitgeschrieben …</div>';
|
||||||
|
}
|
||||||
|
const t0 = v[0][0], t1 = v[v.length - 1][0];
|
||||||
|
const max = Math.max(10, ...v.map(p => p[1]));
|
||||||
|
const X = t => 4 + (t - t0) / Math.max(1, t1 - t0) * 272;
|
||||||
|
const Y = w => 58 - w / max * 50;
|
||||||
|
const pfad = v.map((p, i) => (i ? "L" : "M") + X(p[0]).toFixed(1) + " " + Y(p[1]).toFixed(1)).join("");
|
||||||
|
const letzt = v[v.length - 1];
|
||||||
|
const minuten = Math.max(1, Math.round((t1 - t0) / 60000));
|
||||||
|
return `
|
||||||
|
<svg class="anl-funken" viewBox="0 0 280 76" aria-label="Verlauf der letzten ${minuten} Minuten">
|
||||||
|
<line x1="4" x2="276" y1="58" y2="58" class="anl-gitter"/>
|
||||||
|
<path d="${pfad}L${X(t1)} 58L4 58Z" class="anl-funken-flaeche"/>
|
||||||
|
<path d="${pfad}" class="anl-funken-linie"/>
|
||||||
|
<circle cx="${X(letzt[0])}" cy="${Y(letzt[1])}" r="3.5" class="anl-funken-punkt"/>
|
||||||
|
<text x="4" y="73" class="anl-achse">vor ${minuten} min</text>
|
||||||
|
<text x="276" y="73" class="anl-achse" text-anchor="end">jetzt</text>
|
||||||
|
<text x="276" y="10" class="anl-achse" text-anchor="end">${watt(max)}</text>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Anlage = Anlage;
|
||||||
|
})();
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
Katalog der Anlage-Ansicht (Solar-Seite, Karte "Realtime" -> "Anlage").
|
||||||
|
|
||||||
|
Beschreibt die PV-Anlage dieses Hauses: Flächen, Wechselrichter und jede
|
||||||
|
Platte mit ihrer tatsächlichen Lage. Gezeichnet wird er von
|
||||||
|
js/solar/anlage.js, ohne dass der Renderer eine Fläche beim Namen kennt.
|
||||||
|
Die Lagen stammen aus dem früheren Plan in realtime.svg (Einheiten wie
|
||||||
|
dort, Platte 70 x 40, Zeichenfläche etwa 1250 x 740).
|
||||||
|
|
||||||
|
Die Leistung kommt aus solarManager/P_PVn - ein Eintrag je Eingang, in der
|
||||||
|
Reihenfolge von solarManager.py -, der Zustand der Geräte aus
|
||||||
|
solarManager/invertersN/... .
|
||||||
|
|
||||||
|
flaechen [{id, name, farbe, umriss: "x,y x,y ..." | {x,y,w,h}}]
|
||||||
|
wechselrichter[{nr, name, modell, eingaenge: [P_PVn-Index, ...]}]
|
||||||
|
nr = N in solarManager/invertersN
|
||||||
|
strings Platten ohne eigenen Messwert (in Reihe am Gen24):
|
||||||
|
[{id, name, flaeche, eingang, farbe, platten: [[x,y,w,h], ...]}]
|
||||||
|
platten [{eingang, flaeche, nr?, x, y, w, h}] - w/h ohne Angabe 70/40;
|
||||||
|
nr ist die Beschriftung (#14), sonst "Eingang n"
|
||||||
|
nord {x, y, winkel} für den Nordpfeil
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
const ANLAGE_KATALOG = (function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Dach, String A: vier Spalten stehend, darunter liegend, rechts stehend.
|
||||||
|
const dachA = [];
|
||||||
|
[600, 640, 680].forEach(x => [80, 150, 220, 290].forEach(y => dachA.push([x - 40, y, 40, 70])));
|
||||||
|
[80, 150, 220].forEach(y => dachA.push([680, y, 40, 70]));
|
||||||
|
[[790, 290], [720, 330], [790, 330], [870, 290], [870, 330], [940, 330]].forEach(([x, y]) => dachA.push([x, y, 70, 40]));
|
||||||
|
[1160, 1120, 1080].forEach(x => dachA.push([x - 40, 300, 40, 70]));
|
||||||
|
|
||||||
|
// Dach, String B: drei Reihen stehend, etwas breitere Platten.
|
||||||
|
const dachB = [[1160, 200], [1115, 200], [1070, 200], [1160, 130], [1115, 130], [1070, 130], [1025, 130],
|
||||||
|
[1160, 60], [1115, 60], [1070, 60]].map(([x, y]) => [x - 45, y, 45, 70]);
|
||||||
|
|
||||||
|
const stehend = (eingang, x, y) => ({ eingang, flaeche: "veranda", x, y, w: 40, h: 70 });
|
||||||
|
const carport = (eingang, nr, x, y) => ({ eingang, flaeche: "carport", nr, x, y });
|
||||||
|
|
||||||
|
return {
|
||||||
|
flaechen: [
|
||||||
|
{ id: "dach", name: "Dach", farbe: "#f7c000",
|
||||||
|
umriss: ["560,80 720,80 720,300 1160,300 1160,370 560,370", "980,60 1160,60 1160,265 980,265"] },
|
||||||
|
{ id: "veranda", name: "Veranda", farbe: "#f78d00", umriss: { x: 385, y: 35, w: 300, h: 520 } },
|
||||||
|
{ id: "carport", name: "Carport", farbe: "#f75a1e", umriss: { x: 85, y: 495, w: 380, h: 240 } },
|
||||||
|
],
|
||||||
|
|
||||||
|
wechselrichter: [
|
||||||
|
{ nr: 0, name: "Gen24", modell: "Fronius Symo Gen24", eingaenge: [0, 1] },
|
||||||
|
{ nr: 1, name: "Veranda UG2", modell: "Hoymiles (DTU-BI)", eingaenge: [2, 3] },
|
||||||
|
{ nr: 2, name: "Veranda UG/OG1", modell: "Hoymiles, 4 Eingänge", eingaenge: [4, 5, 6, 7] },
|
||||||
|
{ nr: 3, name: "Veranda OG2–4", modell: "Hoymiles, 6 Eingänge", eingaenge: [8, 9, 10, 11, 12, 13] },
|
||||||
|
{ nr: 4, name: "Carport 1–6", modell: "Hoymiles HMS-2250-6T", eingaenge: [14, 15, 16, 17, 18, 19] },
|
||||||
|
{ nr: 5, name: "Carport 7–12", modell: "Hoymiles HMS-2250-6T", eingaenge: [20, 21, 22, 23, 24, 25] },
|
||||||
|
{ nr: 6, name: "Carport 13–16", modell: "Hoymiles HMS-1800-4T", eingaenge: [26, 27, 28, 29] },
|
||||||
|
{ nr: 7, name: "Carport 17–20", modell: "Hoymiles HMS-1600-4T", eingaenge: [30, 31, 32, 33] },
|
||||||
|
],
|
||||||
|
|
||||||
|
strings: [
|
||||||
|
{ id: "A", name: "String A", flaeche: "dach", eingang: 0, farbe: "#f7c000", platten: dachA, schild: [640, 222] },
|
||||||
|
{ id: "B", name: "String B", flaeche: "dach", eingang: 1, farbe: "#f7a000", platten: dachB, schild: [1070, 168] },
|
||||||
|
],
|
||||||
|
|
||||||
|
platten: [
|
||||||
|
stehend(7, 400, 50), stehend(6, 400, 120), stehend(3, 400, 200), stehend(2, 400, 270),
|
||||||
|
stehend(5, 460, 50), stehend(4, 460, 120), stehend(9, 460, 200), stehend(8, 460, 270),
|
||||||
|
stehend(11, 460, 350), stehend(10, 460, 420),
|
||||||
|
{ eingang: 12, flaeche: "veranda", x: 500, y: 500 }, { eingang: 13, flaeche: "veranda", x: 600, y: 500 },
|
||||||
|
|
||||||
|
carport(14, 1, 100, 510), carport(15, 2, 100, 550), carport(16, 3, 100, 590), carport(17, 4, 100, 630),
|
||||||
|
carport(21, 7, 170, 550), carport(20, 8, 170, 510), carport(19, 6, 170, 590), carport(18, 5, 170, 630),
|
||||||
|
carport(25, 12, 240, 510), carport(24, 11, 240, 550), carport(23, 10, 240, 590), carport(22, 9, 240, 630),
|
||||||
|
carport(29, 16, 310, 510), carport(28, 15, 310, 550), carport(27, 14, 310, 590), carport(26, 13, 310, 630),
|
||||||
|
carport(32, 19, 380, 550), carport(31, 18, 380, 590), carport(30, 17, 380, 630),
|
||||||
|
carport(33, 20, 200, 680),
|
||||||
|
],
|
||||||
|
|
||||||
|
nord: { x: 1235, y: 45, winkel: 10 },
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -48,6 +48,24 @@ const ENERGIEFLUSS_KATALOG = (function () {
|
|||||||
|
|
||||||
const kw = w => (Math.abs(w) / 1000).toFixed(1).replace(".", ",");
|
const kw = w => (Math.abs(w) / 1000).toFixed(1).replace(".", ",");
|
||||||
|
|
||||||
|
/** Flächen des Sonnenrings aus anlageKatalog.js - fehlt der, feste Vorgabe. */
|
||||||
|
function flaechenAusAnlage() {
|
||||||
|
if (typeof ANLAGE_KATALOG === "undefined") {
|
||||||
|
return [
|
||||||
|
{ name: "Dach", von: 0, bis: 1, farbe: "#f7c000" },
|
||||||
|
{ name: "Veranda", von: 2, bis: 13, farbe: "#f78d00" },
|
||||||
|
{ name: "Carport", von: 14, bis: 33, farbe: "#f75a1e" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return ANLAGE_KATALOG.flaechen.map(f => {
|
||||||
|
const eingaenge = [
|
||||||
|
...ANLAGE_KATALOG.platten.filter(p => p.flaeche === f.id).map(p => p.eingang),
|
||||||
|
...ANLAGE_KATALOG.strings.filter(s => s.flaeche === f.id).map(s => s.eingang),
|
||||||
|
];
|
||||||
|
return { name: f.name, von: Math.min(...eingaenge), bis: Math.max(...eingaenge), farbe: f.farbe };
|
||||||
|
}).filter(f => isFinite(f.von));
|
||||||
|
}
|
||||||
|
|
||||||
const wallboxZeichen = (modus, stecker, gesperrt) => {
|
const wallboxZeichen = (modus, stecker, gesperrt) => {
|
||||||
const z = [];
|
const z = [];
|
||||||
if (modus === "Eco") z.push({ art: "eco", titel: "Eco-Modus" });
|
if (modus === "Eco") z.push({ art: "eco", titel: "Eco-Modus" });
|
||||||
@@ -65,14 +83,9 @@ const ENERGIEFLUSS_KATALOG = (function () {
|
|||||||
eltern: "wr", x: 180, y: 130, skala: 15000,
|
eltern: "wr", x: 180, y: 130, skala: 15000,
|
||||||
klick: { ansicht: "anlage" },
|
klick: { ansicht: "anlage" },
|
||||||
// Welche Einträge von solarManager/P_PVn zu welcher Fläche gehören.
|
// Welche Einträge von solarManager/P_PVn zu welcher Fläche gehören.
|
||||||
// Reihenfolge wie in solarManager.py.
|
// Vorgabe aus dem Anlagenkatalog, damit die Zuordnung nur einmal
|
||||||
optionen: {
|
// gepflegt wird; in den Einstellungen überschreibbar.
|
||||||
flaechen: [
|
optionen: { flaechen: flaechenAusAnlage() },
|
||||||
{ 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) {
|
lesen(m, opt) {
|
||||||
const pvn = m.json("solarManager/P_PVn") || [];
|
const pvn = m.json("solarManager/P_PVn") || [];
|
||||||
const pv = m.zahl("solarManager/P_PV");
|
const pv = m.zahl("solarManager/P_PV");
|
||||||
|
|||||||
+13
-21
@@ -142,11 +142,12 @@ 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.
|
// Die Energiefluss-Uebersicht (js/solar/energiefluss.js) und die Anlage-Ansicht
|
||||||
// Bis September 2026 stand hier updateValuesMQTT fuer das feste realtime.svg -
|
// (js/solar/anlage.js). Bis September 2026 stand hier updateValuesMQTT fuer das
|
||||||
// jeder Kreis mit eigenen Zeilen. Was ein Kreis zeigt, steht jetzt im Katalog
|
// feste realtime.svg - jeder Kreis mit eigenen Zeilen. Was gezeigt wird, steht
|
||||||
// (energieflussKatalog.js); hier bleibt nur der Lageplan hinter der Sonne.
|
// jetzt in den Katalogen (energieflussKatalog.js, anlageKatalog.js).
|
||||||
let energiefluss = null;
|
let energiefluss = null;
|
||||||
|
let anlage = null;
|
||||||
|
|
||||||
const solarSVG = {
|
const solarSVG = {
|
||||||
updateCnt: 99,
|
updateCnt: 99,
|
||||||
@@ -163,23 +164,8 @@ const solarSVG = {
|
|||||||
},
|
},
|
||||||
updateValuesMQTT: function (mqttData) {
|
updateValuesMQTT: function (mqttData) {
|
||||||
if (energiefluss) energiefluss.aktualisieren(mqttData);
|
if (energiefluss) energiefluss.aktualisieren(mqttData);
|
||||||
|
// Auch verborgen: so schreibt die Anlage ihren Verlauf je Platte mit.
|
||||||
// Lageplan (assets/img/pvplan.svg): Leistung je Eingang und die Liste
|
if (anlage) anlage.aktualisieren(mqttData);
|
||||||
// der Wechselrichter.
|
|
||||||
let pvn = [];
|
|
||||||
try { pvn = JSON.parse(mqttData["solarManager/P_PVn"]); } catch (e) { pvn = []; }
|
|
||||||
for (let i = 0; i < pvn.length; i++) {
|
|
||||||
if (svgElement("det_pv" + i + "P")) setText("det_pv" + i + "P", powerToString(pvn[i]));
|
|
||||||
}
|
|
||||||
let liste = "";
|
|
||||||
for (let i = 0; mqttData["solarManager/inverters" + i + "/name"] !== undefined; i++) {
|
|
||||||
const basis = "solarManager/inverters" + i;
|
|
||||||
const gestoert = mqttData[basis + "/error"] > 0;
|
|
||||||
liste += '<tspan x="0" dy="1.2em">' + mqttData[basis + "/name"] + ":</tspan>"
|
|
||||||
+ '<tspan x="160">' + (gestoert ? "--" : Math.round(mqttData[basis + "/temp"])) + " °C</tspan>"
|
|
||||||
+ '<tspan x="215">' + (gestoert ? "-- W" : powerToString(mqttData[basis + "/p_AC"])) + "</tspan>";
|
|
||||||
}
|
|
||||||
if (svgElement("invList")) setHTML("invList", liste);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -220,6 +206,12 @@ function energieflussStarten() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
energiefluss.zeichnen();
|
energiefluss.zeichnen();
|
||||||
|
|
||||||
|
const anlageBehaelter = document.getElementById("anlage");
|
||||||
|
if (anlageBehaelter && typeof Anlage !== "undefined") {
|
||||||
|
anlage = new Anlage(anlageBehaelter);
|
||||||
|
anlage.zeichnen();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var chartSettings = {
|
var chartSettings = {
|
||||||
|
|||||||
@@ -68,9 +68,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body p-2 p-md-3">
|
<div class="card-body p-2 p-md-3">
|
||||||
<div class="enf-ansicht enf-behaelter" data-enf-ansicht-flaeche="fluss" id="energiefluss"></div>
|
<div class="enf-ansicht enf-behaelter" data-enf-ansicht-flaeche="fluss" id="energiefluss"></div>
|
||||||
<div class="enf-ansicht" data-enf-ansicht-flaeche="anlage" hidden>
|
<!-- Lageplan der PV-Anlage: js/solar/anlage.js aus anlageKatalog.js -->
|
||||||
<?php readfile(__DIR__ . "/../assets/img/pvplan.svg"); ?>
|
<div class="enf-ansicht" data-enf-ansicht-flaeche="anlage" id="anlage" hidden></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user