Dashboard: Doppelungen zusammengelegt, Lade- und Renderlast gesenkt
Zusammenlegung
- Die sechs Endpunkte getConsData_/getProdData_{month,year,decade}.php sind
ajax/energyHistory.php?series=&range= gewichen, der gemeinsame Aufbau der
Chart-Daten steht in ajax/chartData.php. Die erzeugte SQL wurde gegen alle
sechs Originale abgeglichen.
- Die 13 Raeume der Home-Ansicht stehen nur noch in restricted/rooms.php.
home.php rendert die SVG-Kacheln daraus, homeMQTT.js bekommt dieselbe
Tabelle als JSON (346 -> 222 bzw. 428 -> 197 Zeilen).
- Gemeinsame JS-Helfer in js/solar/common.js; geteilte Bausteine fuer
Wetterkacheln, Chart-Skripte und das Formular der Ladesteuerung.
- Seitenvorlagen sind jetzt .php und werden eingebunden, statt per
file_get_contents zusammengesetzt zu werden.
Fehlerbehebungen
- ?action=weather fuehrte auf eine leere Seite, es gibt jetzt weather.php.
- homeMQTT.js griff auf ein #meteogram zu, das home.php nie hatte, und brach
damit den readystatechange-Handler ab.
- Die Heizungsseite abonnierte weatherStation/#, zeigte die Werte aber nie an.
Ladelast
- Chart.js, MQTT und Meteogramm werden nur noch auf den Seiten geladen, die
sie brauchen; die Seitentabelle dafuer steht in index.php.
- mqtt.js durch die minifizierte Fassung ersetzt (859 -> 359 KB). MQTT.js
5.14.1, byte-identisch mit dem signierten npm-Tarball.
- Rund 3,1 MB ungenutzte Vendor-Reste und AdminLTE-Demobilder entfernt.
- Solar 1796 -> 1240 KB, Home 1796 -> 873 KB, Historie 1796 -> 843 KB.
Speicher im Browser
- Chart.defaults.devicePixelRatio auf 1.5 gedeckelt. Die Zeichenpuffer der
Diagramme waren mit 20-45 MB der groesste Posten der Seite.
- Das Meteogramm-iframe laedt erst beim Hinscrollen.
Realtime-SVG
- Filterbereiche auf das noetige Mass verkleinert und den wirkungslosen
feOffset-Durchgang entfernt: 60 % weniger gerasterte Filterflaeche bei
pixelgleichem Ergebnis.
- Die Fluss-Animation laeuft ueber transform statt stroke-dashoffset, mit
Punkten statt Strichmuster; sie ruht, wo kein Fluss anliegt.
- Der Wertupdate schreibt nur noch bei echter Aenderung und benutzt
textContent statt innerHTML: 9 statt 86 Elemente pro Update, davon keines
mit Filter. describeArc rundet auf volle Grad.
- waterInfo auf scale(0.90) wie die uebrigen Info-Gruppen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Gemeinsame Bausteine der Dashboard-Seiten.
|
||||
* Wird vor solarMQTT.js / homeMQTT.js / heatMQTT.js / historyMQTT.js geladen.
|
||||
*/
|
||||
|
||||
/** Sekunden als "HH:MM". */
|
||||
String.prototype.toHHMM = function () {
|
||||
var sec_num = parseInt(this, 10);
|
||||
var hours = Math.floor(sec_num / 3600);
|
||||
var minutes = Math.floor((sec_num - (hours * 3600)) / 60);
|
||||
if (hours < 10) { hours = "0" + hours; }
|
||||
if (minutes < 10) { minutes = "0" + minutes; }
|
||||
return hours + ':' + minutes;
|
||||
};
|
||||
|
||||
function powerToString(power) {
|
||||
if (Math.abs(power) > 999) {
|
||||
power = power / 1000;
|
||||
return power.toPrecision(3) + " kW";
|
||||
} else {
|
||||
return Math.round(power) + " W";
|
||||
}
|
||||
}
|
||||
|
||||
function loadingHTML(msg) {
|
||||
return "<div class='m-4'><div class='spinner-border' role='status'><span class='visually-hidden'>Loading...</span></div> " + msg + "</div>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagrammdaten holen und eintragen.
|
||||
* params Funktion, die {FROM, TO} liefert (Zeitraum aendert sich zur Laufzeit)
|
||||
* sunrise true -> Sonnenstands-Markierungen fuer denselben Zeitraum
|
||||
* Objekt {FROM, TO} -> fester Zeitraum
|
||||
* autoUpdate alle 5 Minuten neu laden
|
||||
* updateMode an chart.update() durchgereicht ("none" = ohne Animation)
|
||||
*/
|
||||
async function getData(chart, url, options = {}) {
|
||||
const {
|
||||
params = null,
|
||||
sunrise = false,
|
||||
autoUpdate = true,
|
||||
updateMode = undefined,
|
||||
refreshMs = 5 * 60 * 1000
|
||||
} = options;
|
||||
|
||||
const range = params ? params() : null;
|
||||
const query = range ? "?FROM=" + range.FROM + "&TO=" + range.TO : "";
|
||||
|
||||
try {
|
||||
chart.options.events = []; // waehrend des Ladens keine Tooltips
|
||||
const response = await fetch(url + query);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Response status: ${response.status}`);
|
||||
}
|
||||
chart.data = await response.json();
|
||||
|
||||
if (sunrise) {
|
||||
const sunRange = sunrise === true ? range : sunrise;
|
||||
const response2 = await fetch("ajax/getSunrise.php?FROM=" + sunRange.FROM + "&TO=" + sunRange.TO);
|
||||
if (!response2.ok) {
|
||||
throw new Error(`Response status: ${response2.status}`);
|
||||
}
|
||||
chart.options.plugins.annotation.annotations = await response2.json();
|
||||
}
|
||||
chart.options.events = ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'];
|
||||
chart.update(updateMode);
|
||||
} catch (error) {
|
||||
console.log("getData " + url + ": " + error.message);
|
||||
}
|
||||
|
||||
if (autoUpdate) {
|
||||
setTimeout(function () { getData(chart, url, options); }, refreshMs);
|
||||
}
|
||||
}
|
||||
|
||||
/** Vorgerendertes HTML-Schnipsel in ein Element laden. */
|
||||
async function getStats(elem_id, url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Response status: ${response.status}`);
|
||||
}
|
||||
document.getElementById(elem_id).innerHTML = await response.text();
|
||||
} catch (error) {
|
||||
console.log("getStats " + url + ": " + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formular des Modals per POST abschicken.
|
||||
* Der Start/Stop-Knopf der Ladesteuerung schickt nur sich selbst.
|
||||
*/
|
||||
function submitFormAjax(event) {
|
||||
let xmlhttp = new XMLHttpRequest();
|
||||
|
||||
xmlhttp.onreadystatechange = function () {
|
||||
if (this.readyState === 4 && this.status === 200) {
|
||||
setTimeout(function () { modalEV.hide(); }, 700);
|
||||
if (xmlhttp.responseText) {
|
||||
alert(xmlhttp.responseText);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let post = "";
|
||||
if (event.currentTarget.id == "evStart/Stop") {
|
||||
post = "evStart/Stop=" + event.currentTarget.innerHTML;
|
||||
} else {
|
||||
const form = document.getElementById('modalEV').querySelector('form');
|
||||
for (const element of Array.from(form.elements)) {
|
||||
if (!element.name) continue;
|
||||
if (element.type == "radio" || element.type == "checkbox") {
|
||||
if (element.checked) {
|
||||
post += element.name + "=" + encodeURIComponent(element.value) + "&";
|
||||
}
|
||||
} else if (element.type == "button") {
|
||||
if (!element.classList.contains("accordion-button")) {
|
||||
post += element.name + "=" + encodeURIComponent(element.innerHTML) + "&";
|
||||
}
|
||||
} else {
|
||||
post += element.name + "=" + encodeURIComponent(element.value) + "&";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
xmlhttp.open("POST", event.currentTarget.contentURL, true);
|
||||
xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
document.getElementById('modalEV').querySelector('.modal-body').innerHTML = loadingHTML("Änderungen werden übernommen...");
|
||||
xmlhttp.send(post);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schieberegler im Modal mit seiner Beschriftung verbinden.
|
||||
* label(wert) liefert den anzuzeigenden Text.
|
||||
*/
|
||||
function bindModalSlider(slider, span, label) {
|
||||
slider.oninput = function () {
|
||||
const min = Number(slider.min);
|
||||
const max = Number(slider.max);
|
||||
const percent = (Number(slider.value) - min) * 100 / (max - min);
|
||||
slider.style.setProperty("--background-size", `${percent}%`);
|
||||
|
||||
// Beschriftung mitlaufen lassen, an den Raendern etwas gebremst
|
||||
let margin = percent;
|
||||
if (margin < 10) {
|
||||
margin = margin - 0.4 * margin;
|
||||
} else if (margin < 85) {
|
||||
margin = margin - 4;
|
||||
} else {
|
||||
margin = margin - 4 - 0.5 * (margin - 85);
|
||||
}
|
||||
span.setAttribute('style', 'margin-left:' + margin + '%;');
|
||||
span.innerHTML = label(slider.value);
|
||||
};
|
||||
}
|
||||
|
||||
/** Meteoblue-Meteogramm einhängen, sofern die Seite einen Platz dafür hat. */
|
||||
function mountMeteogram() {
|
||||
const target = document.getElementById("meteogram");
|
||||
if (!target) return;
|
||||
// loading="lazy": das Meteogramm ist ein vollstaendiges Fremd-Dokument und
|
||||
// liegt unterhalb der Falz - es wird erst geladen, wenn man hinscrollt.
|
||||
target.innerHTML = "<iframe loading='lazy' src='" + meteogramURL + "' frameborder='0' scrolling=NO' allowtransparency='true' sandbox='allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox' style='width: 100%;height: 500px;border: 0;overflow: hidden;'></iframe><!-- DO NOT REMOVE THIS LINK --><a href='https://www.meteoblue.com/de/wetter/woche/index' target='_blank' rel='noopener'></a>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Wetterkacheln aktualisieren (restricted/weather_cards.html).
|
||||
* value(name) liefert den Wert zu weatherStation/<name> - die Seiten legen
|
||||
* die MQTT-Werte unterschiedlich ab, deshalb der Zugriff über eine Funktion.
|
||||
*/
|
||||
function updateWeatherCards(value) {
|
||||
const windDir = document.getElementById("windDir");
|
||||
if (!windDir) return; // Seite ohne Wetterkacheln
|
||||
windDir.style.transform = "rotate(" + (90 + Number(value("windDeg"))) + "deg)";
|
||||
document.getElementById("windSpd").innerHTML = value("avgWindspeed");
|
||||
document.getElementById("humidity").innerHTML = value("hum");
|
||||
document.getElementById("temp").innerHTML = value("tempAmb");
|
||||
document.getElementById("windDirGust").style.transform = "rotate(" + (90 + Number(value("gustDeg"))) + "deg)";
|
||||
document.getElementById("windGust").innerHTML = value("maxgust");
|
||||
document.getElementById("ambPress").innerHTML = Math.round(value("qff") * 10) / 10;
|
||||
}
|
||||
Reference in New Issue
Block a user