Nach dem abgestimmten Entwurf: Ladestand als Ring mit Ziel, Leistung, Sonnenanteil und fertig um; ein grosser Knopf, der sich nach der gemeldeten Freigabe (frc) richtet - "Laden stoppen", "Ladung freigeben" (zurueck zum Modus statt dauerhaft gesperrt), "Jetzt laden", ohne Auto ausgegraut. Modus als Karten (Sofort, Sonne, Geplant), Planung nur bei "Geplant": mit Ladestand des Autos als Ziel-Ladestand, sonst als zusaetzliche Prozent; Abfahrt per Schnellwahl. Ladestrom als Segmente mit kW. Jede Aenderung geht sofort als JSON an carSteuerung.php (frc, modus, strom, plan) statt ueber "Save changes"; die Logik steht in js/solar/ladefenster.js. Am 800er-Display zweispaltig ohne Scrollen. Nebenbei: die Uebersicht las die Restladezeit aus carRemChrg, das der Manager nicht mehr schickt - jetzt evRemChrgTime. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
247 lines
9.4 KiB
JavaScript
247 lines
9.4 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Formular des Modals per POST abschicken ("Save changes", heute nur noch
|
|
* der Heizstab). Das Ladefenster schreibt selbst, siehe ladefenster.js.
|
|
*/
|
|
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 = "";
|
|
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;
|
|
}
|
|
|
|
|
|
/**
|
|
* Offenen Reiter in der Adresse merken.
|
|
*
|
|
* Gilt fuer jede Leiste, die restricted/reiter.php mit "merken" gebaut hat -
|
|
* erkennbar an data-reiter-merken. Ohne das landet man nach jedem Neuladen
|
|
* wieder im ersten Reiter, obwohl man gerade woanders gearbeitet hat.
|
|
*
|
|
* In der Adresse steht nur das Kuerzel des Reiters (#kacheln), nicht der Name
|
|
* der Leiste: es ist eine Adresse, die man auch von Hand tippt. Zwei merkende
|
|
* Leisten auf einer Seite waeren damit nicht zu unterscheiden - dann muesste
|
|
* hier der Name davor, und die Adresse waere haesslicher als noetig.
|
|
*/
|
|
function reiterMerken() {
|
|
if (typeof bootstrap === "undefined") return;
|
|
|
|
document.querySelectorAll("[data-reiter-merken]").forEach(function (leiste) {
|
|
const knoepfe = leiste.querySelectorAll("[data-reiter]");
|
|
const gewuenscht = location.hash.replace("#", "");
|
|
|
|
knoepfe.forEach(function (knopf) {
|
|
knopf.addEventListener("shown.bs.tab", function () {
|
|
history.replaceState(null, "", "#" + knopf.dataset.reiter);
|
|
});
|
|
if (gewuenscht && knopf.dataset.reiter === gewuenscht) {
|
|
new bootstrap.Tab(knopf).show();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", reiterMerken);
|