Auf dem Handy war die Seite bisher nur mit der Desktopversion des Browsers brauchbar. Drei Ursachen, alle nachgemessen bei 375 px: Die Diagramme standen im Hochformat, 301 x 400 px - die Zeitachse lag auf der kurzen Seite. Die Hoehe kam nicht aus dem Container: .chart-container hatte inline height:100% in einem Elternelement ohne Hoehe, und die Canvas trugen height="400px;", was kein gueltiger Zahlenwert ist. Die Hoehe steht jetzt im Stylesheet, 400 px am Rechner und 250 px unter 768 px Breite, die Beschriftung dort 8 statt 10 px. Das Meteogramm hat sich nicht angepasst, sondern gequetscht. meteoblue legt sein Bild einmal beim Laden fest und zeichnet es bei einer Breitenaenderung nicht neu; unter etwa 600 px ueberlagern sich Wochentage und Stundenachse. Es wird jetzt in mindestens 640 px Breite geladen - auf breiten Karten gleich in Kartenbreite, dort aendert sich nichts - und, wenn die Karte schmaler ist, als Ganzes verkleinert. Zuschnitt und Skalierung setzt das Skript direkt am Element, nicht ueber das Stylesheet: sonst haengt die Darstellung an einer womoeglich veralteten solar.css im Browser-Cache, und der breit geladene Rahmen ragt ungeschnitten aus der Karte. Passend dazu bekommt solar.css eine Versionskennung aus filemtime(), wie sie die Skripte in footer.php und adminlte.min.css laengst tragen. Ohne sie kam auf schon einmal besuchten Geraeten die alte Datei aus dem Cache. Die Statistik-Kacheln lagen in nackten .col ohne Breitenangabe; Bootstrap laesst sie dann nach Textlaenge umbrechen, mal zwei, mal drei je Zeile und unterschiedlich breit. Die Breite kommt jetzt aus row-cols-2/row-cols-md-4, und die Karte fuellt die Zeilenhoehe, damit die Werte trotz ein- oder zweizeiliger Titel auf einer Linie stehen. Zuletzt sind die Abstaende unter 768 px enger gefasst - auf .app-content beschraenkt, damit Seitenleiste, Kopfzeile und Modals unberuehrt bleiben: Spaltenabstand 24 -> 8 px, Kartenabstand 24 -> 12 px, Polsterung 16 -> 8 px. Die nutzbare Breite steigt damit von 301 auf 317 px. Geprueft bei 375 und 1200 px auf Solar, Historie, Heizung, Home und Wetter: kein Querlauf, am Rechner unveraenderte Werte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
231 lines
8.7 KiB
JavaScript
231 lines
8.7 KiB
JavaScript
/*
|
|
* 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. */
|
|
// Mindestbreite, in der das Meteogramm von meteoblue geladen wird. Der
|
|
// Anbieter legt sein Bild einmal beim Laden fest und zeichnet es bei einer
|
|
// Breitenaenderung nicht neu; unterhalb von etwa 600 px ueberlagern sich
|
|
// Wochentage und Stundenachse zu einem unlesbaren Streifen. Auf schmalen
|
|
// Schirmen wird es deshalb in dieser Breite geladen und als Ganzes
|
|
// verkleinert - dieselbe Ansicht wie am Rechner, nur kleiner. Ist die Karte
|
|
// breiter, wird gleich in Kartenbreite geladen und nichts skaliert.
|
|
const METEOGRAM_BREITE = 640;
|
|
const METEOGRAM_HOEHE = 500;
|
|
|
|
function mountMeteogram() {
|
|
const ziel = document.getElementById("meteogram");
|
|
if (!ziel) return;
|
|
// loading="lazy": das Meteogramm ist ein vollstaendiges Fremd-Dokument und
|
|
// liegt unterhalb der Falz - es wird erst geladen, wenn man hinscrollt.
|
|
const stil = window.getComputedStyle(ziel);
|
|
const innenBreite = ziel.clientWidth - parseFloat(stil.paddingLeft) - parseFloat(stil.paddingRight);
|
|
const ladeBreite = Math.max(METEOGRAM_BREITE, Math.round(innenBreite) || 0);
|
|
ziel.innerHTML =
|
|
"<div class='meteogram-buehne'>"
|
|
+ "<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:" + ladeBreite + "px;height:" + METEOGRAM_HOEHE + "px;border:0;overflow:hidden;'></iframe>"
|
|
+ "</div>"
|
|
+ "<!-- DO NOT REMOVE THIS LINK -->"
|
|
+ "<a href='https://www.meteoblue.com/de/wetter/woche/index' target='_blank' rel='noopener'></a>";
|
|
|
|
const buehne = ziel.querySelector(".meteogram-buehne");
|
|
const rahmen = buehne.querySelector("iframe");
|
|
// Zuschnitt und Skalierung stehen hier und nicht im Stylesheet: sonst
|
|
// haengt die Darstellung an einer womoeglich veralteten solar.css im
|
|
// Browser-Cache, und der breit geladene Rahmen ragte ungeschnitten aus
|
|
// der Karte heraus.
|
|
buehne.style.overflow = "hidden";
|
|
rahmen.style.display = "block";
|
|
rahmen.style.transformOrigin = "0 0";
|
|
let letzteBreite = 0;
|
|
const anpassen = function () {
|
|
const breite = buehne.clientWidth;
|
|
if (breite === 0 || breite === letzteBreite) return;
|
|
letzteBreite = breite;
|
|
const faktor = Math.min(1, breite / ladeBreite);
|
|
rahmen.style.transform = "scale(" + faktor + ")";
|
|
buehne.style.height = Math.round(METEOGRAM_HOEHE * faktor) + "px";
|
|
};
|
|
anpassen();
|
|
// Nur auf Breitenaenderungen reagieren - die Hoehe setzt anpassen() selbst,
|
|
// sonst riefe sich der Beobachter gegenseitig auf.
|
|
if (window.ResizeObserver) {
|
|
new ResizeObserver(anpassen).observe(buehne);
|
|
} else {
|
|
window.addEventListener("resize", anpassen);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|