Skoda: Ladefenster im neuen Stil, Ladeziel einstellbar
Laden oeffnet jetzt ein Fenster wie auf der Solarseite: Ring mit Ladestand und Ladeziel, ein Knopf zum Starten oder Stoppen, das Ladeziel in Stufen von 50 bis 100 Prozent. skodaCmd.php kennt dafuer den Befehl ladeziel (PUT charging/limit) und prueft die Stufe, bevor ein Befehl aus dem Kontingent draufgeht. Gesendet wird erst, wenn eine Weile keine andere Stufe gewaehlt wurde; bis das Fahrzeug das Ziel meldet, steht die eigene Wahl im Fenster. Ohne Kabel ist der Start gesperrt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -632,6 +632,7 @@ function zeichneLive() {
|
||||
zeichneAbzeichen(w);
|
||||
zeichneFahrt(w);
|
||||
zeichneLadequelle(w);
|
||||
zeichneLadefenster(w);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------- Diagramme */
|
||||
@@ -1448,6 +1449,10 @@ function koppleSteuerung() {
|
||||
|
||||
/** Das Modal mit den Befehlen eines Bereichs füllen und zeigen. */
|
||||
function zeigeSteuerung(bereich) {
|
||||
if (bereich === "laden") {
|
||||
zeigeLadefenster();
|
||||
return;
|
||||
}
|
||||
const b = STEUERUNG[bereich];
|
||||
if (!b || !steuerModal) return;
|
||||
|
||||
@@ -1530,6 +1535,185 @@ function verdrahteBefehle(bereich) {
|
||||
});
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ Ladefenster */
|
||||
|
||||
/*
|
||||
* Laden im Stil der Bedienfenster der Solarseite (.lade in css/solar.css):
|
||||
* links der Ring mit Ladestand und Ladeziel und der eine grosse Knopf zum
|
||||
* Starten oder Stoppen, rechts die Wahl des Ladeziels.
|
||||
*
|
||||
* Jeder Befehl kostet eine der vier Anfragen je Stunde (skodaCmd.php). Das
|
||||
* Ladeziel wird deshalb nicht bei jedem Antippen geschickt, sondern erst,
|
||||
* wenn eine Weile keine andere Stufe mehr gewaehlt wurde. Bis das Fahrzeug
|
||||
* das neue Ziel meldet - das dauert bis zum naechsten Abruf -, steht die
|
||||
* eigene Wahl im Fenster und ist als "wird übernommen" gekennzeichnet.
|
||||
*/
|
||||
const LADEZIELE = [50, 60, 70, 80, 90, 100];
|
||||
const LADEZIEL_WARTEN_MS = 1500;
|
||||
const LADEZIEL_HALTEN_MS = 15 * 60 * 1000;
|
||||
let ladezielWunsch = null; // { prozent, seit, gesendet }
|
||||
let ladezielUhr = null;
|
||||
|
||||
function zeigeLadefenster() {
|
||||
if (!steuerModal) return;
|
||||
document.getElementById("modal-title").textContent = "Laden";
|
||||
const speichern = document.getElementById("modalSaveBtn");
|
||||
if (speichern) speichern.style.display = "none";
|
||||
|
||||
const S = "\u2009";
|
||||
document.getElementById("modal-body").innerHTML =
|
||||
'<div class="lade skoda-lade" id="skodaLadefenster">'
|
||||
+ '<div class="lade-links">'
|
||||
+ '<div class="lade-status">'
|
||||
+ '<div class="lade-ring" aria-hidden="true"><svg viewBox="0 0 120 120">'
|
||||
+ '<circle class="lade-spur" cx="60" cy="60" r="50" pathLength="100"/>'
|
||||
+ '<circle class="lade-ziel" cx="60" cy="60" r="50" pathLength="100"/>'
|
||||
+ '<circle class="lade-ist" cx="60" cy="60" r="50" pathLength="100"/>'
|
||||
+ '</svg><div class="lade-ring-mitte"><b data-sl="soc">–</b><span data-sl="ringZiel"></span></div></div>'
|
||||
+ '<div class="lade-text">'
|
||||
+ '<div class="lade-auto">Antriebsbatterie <span class="lade-pille" data-sl="zustand"></span></div>'
|
||||
+ '<div class="lade-gross" data-sl="gross">–</div>'
|
||||
+ '<div class="lade-zeile" data-sl="zeile1"></div>'
|
||||
+ '<div class="lade-zeile" data-sl="zeile2"></div>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '<button type="button" class="lade-start" data-sl="start"><span class="lade-punkt"></span><span data-sl="startText">…</span></button>'
|
||||
+ '<div class="lade-erklaerung" data-sl="erklaerung"></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="lade-rechts">'
|
||||
+ '<div class="lade-abschnitt">'
|
||||
+ '<h3 id="skodaLadezielTitel">Ladeziel</h3>'
|
||||
+ '<div class="lade-strom skoda-ziele" role="radiogroup" aria-labelledby="skodaLadezielTitel">'
|
||||
+ LADEZIELE.map(p => '<button type="button" role="radio" aria-checked="false" data-ziel="' + p + '"><b>' + p + S + '%</b><span>'
|
||||
+ (p === 80 ? "schonend" : p === 100 ? "Reise" : "") + '</span></button>').join("")
|
||||
+ '</div>'
|
||||
+ '<div class="lade-zeile" data-sl="zielHinweis"></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="lade-meldung" data-sl="meldung" aria-live="polite"></div>'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
|
||||
const fenster = $("skodaLadefenster");
|
||||
fenster.querySelectorAll("[data-ziel]").forEach(k => k.addEventListener("click", () => {
|
||||
ladezielWunsch = { prozent: Number(k.dataset.ziel), seit: Date.now(), gesendet: false };
|
||||
zeichneLadefenster(liveWerte());
|
||||
clearTimeout(ladezielUhr);
|
||||
ladezielUhr = setTimeout(ladezielSenden, LADEZIEL_WARTEN_MS);
|
||||
}));
|
||||
fenster.querySelector("[data-sl=start]").addEventListener("click", () => {
|
||||
const w = liveWerte();
|
||||
skodaBefehl(w.laedt ? "laden_stop" : "laden_start", {});
|
||||
});
|
||||
ladefensterMeldung("");
|
||||
zeichneLadefenster(liveWerte());
|
||||
steuerModal.show();
|
||||
}
|
||||
|
||||
function ladefensterMeldung(text, art) {
|
||||
const m = document.querySelector("#skodaLadefenster [data-sl=meldung]");
|
||||
if (!m) return;
|
||||
m.className = "lade-meldung" + (art ? " " + art : "");
|
||||
m.textContent = text || "Jeder Befehl ist einer von vier je Stunde. Was das Fahrzeug übernimmt, erscheint beim nächsten Abruf.";
|
||||
}
|
||||
|
||||
async function ladezielSenden() {
|
||||
if (!ladezielWunsch || ladezielWunsch.gesendet) return;
|
||||
const w = liveWerte();
|
||||
// Schon so eingestellt: nichts schicken, keinen Befehl verbrauchen.
|
||||
if (w.ziel === ladezielWunsch.prozent) {
|
||||
ladezielWunsch = null;
|
||||
zeichneLadefenster(w);
|
||||
return;
|
||||
}
|
||||
ladezielWunsch.gesendet = true;
|
||||
const ok = await skodaBefehl("ladeziel", { prozent: ladezielWunsch.prozent });
|
||||
if (!ok) ladezielWunsch = null;
|
||||
zeichneLadefenster(liveWerte());
|
||||
}
|
||||
|
||||
/** Einen Befehl schicken; waehrenddessen ist das Fenster gesperrt. */
|
||||
async function skodaBefehl(befehl, extra) {
|
||||
const fenster = $("skodaLadefenster");
|
||||
if (!fenster) return false;
|
||||
const knoepfe = fenster.querySelectorAll("button");
|
||||
fenster.dataset.sendet = "1";
|
||||
knoepfe.forEach(k => { k.disabled = true; });
|
||||
ladefensterMeldung("Befehl wird gesendet …", "warte");
|
||||
let ok = false;
|
||||
try {
|
||||
const antwort = await fetch("ajax/skodaCmd.php", {
|
||||
method: "POST", body: new URLSearchParams(Object.assign({ befehl: befehl }, extra)), cache: "no-store"
|
||||
});
|
||||
const d = await antwort.json();
|
||||
ok = !!d.ok;
|
||||
ladefensterMeldung(d.text, d.ok ? "gut" : "fehler");
|
||||
// Der Zustand aendert sich im Fahrzeug, nicht in der Tabelle - der
|
||||
// Abruf danach bringt ihn.
|
||||
if (d.ok) setTimeout(ladeLive, 20000);
|
||||
} catch (e) {
|
||||
ladefensterMeldung("Der Server hat nicht geantwortet.", "fehler");
|
||||
} finally {
|
||||
delete fenster.dataset.sendet;
|
||||
knoepfe.forEach(k => { k.disabled = false; });
|
||||
zeichneLadefenster(liveWerte());
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/** Das offene Ladefenster mit den Live-Werten nachziehen. */
|
||||
function zeichneLadefenster(w) {
|
||||
const fenster = $("skodaLadefenster");
|
||||
if (!fenster) return;
|
||||
const feld = name => fenster.querySelector("[data-sl=" + name + "]");
|
||||
const S = "\u2009";
|
||||
|
||||
// Die eigene Wahl gilt, bis das Fahrzeug sie meldet - oder zu lange nicht.
|
||||
if (ladezielWunsch && (w.ziel === ladezielWunsch.prozent || Date.now() - ladezielWunsch.seit > LADEZIEL_HALTEN_MS)) {
|
||||
ladezielWunsch = null;
|
||||
}
|
||||
const ziel = ladezielWunsch ? ladezielWunsch.prozent : w.ziel;
|
||||
const soc = w.soc !== null && Number.isFinite(w.soc) ? w.soc : null;
|
||||
|
||||
fenster.querySelector(".lade-ist").style.strokeDasharray = (soc === null ? 0 : Math.max(0, Math.min(100, soc))) + " 100";
|
||||
fenster.querySelector(".lade-ziel").style.strokeDasharray = (ziel ? ziel : 0) + " 100";
|
||||
feld("soc").textContent = soc === null ? "–" : nf(soc) + S + "%";
|
||||
feld("ringZiel").textContent = ziel ? "Ziel " + ziel + S + "%" : "";
|
||||
|
||||
const zustand = LADEZUSTAND[w.zustand];
|
||||
const pille = feld("zustand");
|
||||
pille.textContent = zustand ? zustand[0] : "";
|
||||
pille.className = "lade-pille" + (w.laedt ? " an" : w.zustand === "CHARGING_INTERRUPTED" ? " warn" : "");
|
||||
|
||||
feld("gross").innerHTML = w.laedt && w.kw
|
||||
? nf(w.kw, 1) + S + "kW<small>lädt</small>"
|
||||
: (w.km === null ? "–" : nf(w.km) + S + "km<small>Reichweite</small>");
|
||||
let fertig = "";
|
||||
if (w.laedt && w.restMin > 0) {
|
||||
const um = new Date(Date.now() + w.restMin * 60000);
|
||||
fertig = "fertig um " + String(um.getHours()).padStart(2, "0") + ":" + String(um.getMinutes()).padStart(2, "0");
|
||||
}
|
||||
feld("zeile1").textContent = w.laedt ? (w.km === null ? "" : nf(w.km) + S + "km Reichweite") : "";
|
||||
feld("zeile2").textContent = fertig;
|
||||
|
||||
// Ohne Kabel lehnt das Fahrzeug den Start ab - und der Versuch kostete
|
||||
// trotzdem einen der vier Befehle.
|
||||
const start = feld("start");
|
||||
const ohneKabel = !w.laedt && w.zustand === "CONNECT_CABLE";
|
||||
start.classList.toggle("laeuft", w.laedt);
|
||||
start.classList.toggle("aus", ohneKabel);
|
||||
if (!fenster.dataset.sendet) start.disabled = ohneKabel;
|
||||
feld("startText").textContent = w.laedt ? "Laden stoppen" : ohneKabel ? "Kabel anstecken" : "Laden starten";
|
||||
feld("erklaerung").textContent = w.zustand === "CONNECT_CABLE"
|
||||
? "Kein Kabel angesteckt. Das Ladeziel lässt sich trotzdem schon einstellen."
|
||||
: "Startet oder stoppt das Laden am Fahrzeug. Die Wallbox bleibt davon unberührt – sie gibt weiter frei, was sie freigibt.";
|
||||
|
||||
fenster.querySelectorAll("[data-ziel]").forEach(k =>
|
||||
k.setAttribute("aria-checked", String(Number(k.dataset.ziel) === ziel)));
|
||||
feld("zielHinweis").textContent = ladezielWunsch
|
||||
? (ladezielWunsch.gesendet ? "wird vom Fahrzeug übernommen …" : "wird gleich geschickt …")
|
||||
: (ziel ? "Das Fahrzeug hört bei " + ziel + S + "% auf zu laden." : "Das Fahrzeug meldet noch kein Ladeziel.");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ Start */
|
||||
|
||||
function alleLaden() {
|
||||
|
||||
Reference in New Issue
Block a user