Skoda: Schluessel in den Einstellungen, Ladungen am Stueck, Wallbox in kW

- neuer Reiter "Fahrzeug": API-Schluessel eintragen und pruefen, ohne dass
  ein Wert je an den Browser zurueckgeht (restricted/skodaKeys.php)
- ajax/skoda.php trennt Ladungen am Zustand statt an 30 Minuten Pause und
  rahmt sie mit der Zeile davor und danach ein
- solarLog_skoda_ladepunkte.sql: Wallbox-Verlauf waehrend einer Ladung
- Kachelformat "leistung_kw" fuer Quellen, die schon Kilowatt melden
- Steckersymbol versteht evPlug als 1/0

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 09:30:18 +02:00
co-authored by Claude Opus 5
parent b01e938905
commit deb81045d5
13 changed files with 1272 additions and 126 deletions
+269 -8
View File
@@ -316,7 +316,7 @@ function kachelQuelle(wert) {
* und aus drei Kuerzeln wuerden drei Zeilen.
*/
function kachelWertKurz(wert, kachel) {
const einheit = wert.format === "leistung" ? "W/kW" : (wert.einheit || "");
const einheit = istLeistung(wert) ? "W/kW" : (wert.einheit || "");
const treffer = wert.wechselrichter ? null : katalogTreffer(wert);
let text;
@@ -568,20 +568,29 @@ function kachelWertZeile(wert, i, kachel) {
+ '<div class="kw-einheit"><input type="text" class="form-control form-control-sm"'
+ ' data-feld="einheit" placeholder="Einheit" maxlength="8"'
+ (wert.format === "leistung" ? " disabled" : "")
+ (istLeistung(wert) ? " disabled" : "")
+ ' value="' + textSchuetzen(wert.einheit || "") + '"></div>'
+ '<div class="kw-stellen"><input type="number" class="form-control form-control-sm"'
+ ' data-feld="stellen" min="0" max="3" placeholder="0"'
+ (wert.format === "leistung" ? " disabled" : "")
+ (istLeistung(wert) ? " disabled" : "")
+ ' value="' + (wert.stellen === undefined ? "" : wert.stellen) + '"></div>'
+ '<div class="kw-symbol">' + kachelSymbolFeld(wert.icon || "") + "</div>"
+ '<div class="kw-schalter">'
+ '<label title="Leistung: skaliert selbst zwischen W und kW">'
+ '<input type="checkbox" class="form-check-input" data-feld="format"'
+ (wert.format === "leistung" ? " checked" : "") + "> kW</label>"
// Leistung: die Anzeige skaliert selbst zwischen W und kW. Gewählt wird
// nur, in welcher Einheit die Quelle meldet - die Wallboxen schicken kW,
// die Zähler W. Früher ein Häkchen, das stillschweigend Watt annahm.
+ '<select class="form-select form-select-sm kw-format" data-feld="format"'
+ ' title="Leistung: in welcher Einheit die Quelle meldet. Angezeigt wird'
+ ' dann selbst in W oder kW.">'
+ '<option value=""' + (istLeistung(wert) ? "" : " selected") + '>Einheit</option>'
+ '<option value="leistung"' + (wert.format === "leistung" ? " selected" : "")
+ '>Leistung aus W</option>'
+ '<option value="leistung_kw"' + (wert.format === "leistung_kw" ? " selected" : "")
+ '>Leistung aus kW</option>'
+ "</select>"
+ '<label title="Vorzeichen umdrehen - für Zähler, die verkehrt herum'
+ ' eingebaut sind">'
+ '<input type="checkbox" class="form-check-input" data-feld="negativ"'
@@ -594,6 +603,11 @@ function kachelWertZeile(wert, i, kachel) {
+ "</div></div>";
}
/** Skaliert der Wert selbst zwischen W und kW? Egal, aus welcher Einheit. */
function istLeistung(wert) {
return wert.format === "leistung" || wert.format === "leistung_kw";
}
/** Fremden Text sicher ins Markup bringen - Topics kommen aus der Datenbank. */
function textSchuetzen(s) {
const d = document.createElement("div");
@@ -639,8 +653,9 @@ function kachelWertLesen(feldZeile) {
const pfad = (hol("pfad") || "").trim();
if (pfad) wert.pfad = pfad;
}
if (hol("format")) {
wert.format = "leistung";
const format = hol("format");
if (format === "leistung" || format === "leistung_kw") {
wert.format = format;
} else {
const einheit = (hol("einheit") || "").trim();
if (einheit) wert.einheit = einheit;
@@ -1130,3 +1145,249 @@ document.addEventListener("DOMContentLoaded", function () {
geraeteBinden();
geraeteHolen();
});
/* ==========================================================================
Fahrzeug: die Zugangsschlüssel der MyŠkoda-Anbindung
--------------------------------------------------------------------------
Sie laufen ab. Bis hierher hieß der Tausch: per SSH auf die NAS, skoda.conf
im Editor öffnen, nicht vertippen. Das gehört in die Einstellungen wie die
Preise und die Geräte.
Was hier nie ankommt, sind die Schlüssel selbst. Die Seite steht im
Heimnetz ohne Anmeldung offen, und ein solcher Schlüssel steuert das
Fahrzeug von überall auf der Welt - Laden, Klima, Standort. Zu sehen sind
nur die letzten vier Zeichen, gerade genug, um zwei auseinanderzuhalten.
Geschrieben wird deshalb auch nur in eine Richtung: ein neuer Wert geht
hinein, ein alter kommt nie heraus.
Der zweite und dritte Schlüssel beschleunigen nichts: das Kontingent der
API gilt gemessen fürs ganze Konto. Sie sind für den Fall da, dass einer
abläuft oder widerrufen wird - dann fragt der Abruf mit dem nächsten
weiter, statt stillzustehen. Deshalb steht unter den Feldern, was gerade
gilt, und nicht, was ein weiterer Schlüssel brächte.
========================================================================== */
let skodaModell = null; // was ?action=skoda-keys liefert, ohne Werte
/* --- Zeichnen ------------------------------------------------------------ */
function zeichneSkoda() {
const ziel = document.getElementById("skodaListe");
if (!ziel || !skodaModell) return;
if (!skodaModell.da) {
ziel.innerHTML = '<div class="alert alert-warning mb-0">'
+ "Die Datei <code>" + textSchuetzen(skodaModell.datei) + "</code> gibt es nicht. "
+ "Sie gehört zum SolarManager und wird dort angelegt ohne sie läuft auch "
+ "der Abruf nicht.</div>";
return;
}
const zeilen = skodaModell.felder.map(skodaZeile).join("");
ziel.innerHTML =
'<div class="skoda-felder">' + zeilen + "</div>"
+ (skodaModell.schreibbar ? "" :
'<div class="alert alert-warning mt-3 mb-0 py-2 small">'
+ "Der Webserver darf <code>" + textSchuetzen(skodaModell.datei)
+ "</code> nicht schreiben. Eintragen lässt sich hier nichts.</div>")
+ '<div class="d-flex flex-wrap align-items-center gap-3 mt-3">'
+ '<button type="button" class="btn btn-primary" id="skodaSpeichern"'
+ (skodaModell.schreibbar ? "" : " disabled") + ">Eintragen</button>"
+ '<span class="skoda-stand small text-body-secondary"></span>'
+ "</div>"
+ skodaBilanz();
}
/** Eine Zeile: was der Schlüssel tut, ob er da ist, und ein Feld dafür. */
function skodaZeile(f) {
const kennung = "skodaFeld-" + f.feld;
const stand = f.gesetzt
? '<span class="badge text-bg-success">hinterlegt</span>'
+ ' <span class="text-body-secondary">endet auf ' + textSchuetzen(f.maske) + "</span>"
: '<span class="badge text-bg-secondary">'
+ (f.pflicht ? "fehlt" : "nicht hinterlegt") + "</span>";
// Entfernen nur, wo es etwas zu entfernen gibt und wo es ohne geht. Der
// erste Schlüssel lässt sich ersetzen, aber nicht abschaffen - ohne ihn
// steht der Abruf.
const weg = (f.gesetzt && !f.pflicht)
? '<button type="button" class="btn btn-outline-danger btn-sm skoda-weg"'
+ ' data-feld="' + textSchuetzen(f.feld) + '" title="Diesen Schlüssel entfernen">'
+ '<i class="bi bi-trash"></i></button>'
: "";
return '<div class="border-top py-3 skoda-feld" data-feld="' + textSchuetzen(f.feld) + '">'
+ '<div class="d-flex flex-wrap align-items-baseline gap-2">'
+ '<label class="fw-semibold mb-0" for="' + kennung + '">'
+ textSchuetzen(f.titel) + "</label>"
+ stand
+ "</div>"
+ '<p class="text-body-secondary small mb-2">' + textSchuetzen(f.zweck) + "</p>"
+ '<div class="input-group input-group-sm">'
+ '<input type="password" class="form-control" id="' + kennung + '"'
+ ' autocomplete="off" spellcheck="false"'
+ ' placeholder="' + (f.gesetzt ? "neuer Schlüssel leer lassen, um ihn zu behalten"
: "Schlüssel einfügen") + '">'
+ '<button type="button" class="btn btn-outline-secondary skoda-zeigen"'
+ ' title="Anzeigen"><i class="bi bi-eye"></i></button>'
+ weg
+ "</div>"
+ "</div>";
}
/** Was die hinterlegten Schlüssel einbringen - die Zahlen dahinter. */
function skodaBilanz() {
const m = skodaModell;
const teile = [];
teile.push(m.abruf + (m.abruf === 1 ? " Schlüssel" : " Schlüssel im Wechsel")
+ " für den Abruf beim Laden und während der Fahrt alle "
+ m.takt + " Sekunden");
teile.push(m.befehle + " Befehle je Stunde");
if (m.ablauf) {
teile.push("nächster Ablauf am " + datumLesbar(String(m.ablauf).slice(0, 10)));
}
return '<p class="text-body-secondary small mt-3 mb-0">'
+ textSchuetzen(teile.join(" · ")) + "<br>"
+ "Ein neuer Schlüssel gilt sofort; der Abruf liest die Datei von selbst neu."
+ "</p>";
}
function skodaStand(text, klasse) {
const ziel = document.querySelector("#skodaListe .skoda-stand");
if (!ziel) return;
ziel.className = "skoda-stand small " + (klasse || "text-body-secondary");
ziel.textContent = text;
clearTimeout(skodaStand._uhr);
if (klasse !== "text-danger") {
skodaStand._uhr = setTimeout(function () {
if (ziel.textContent === text) ziel.textContent = "";
}, 8000);
}
}
/* --- Sprechen mit dem Endpunkt ------------------------------------------- */
async function skodaFrage(action, koerper) {
const antwort = await fetch("ajax/settings.php?action=" + action, koerper ? {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(koerper),
} : { cache: "no-store" });
const d = await antwort.json();
if (!antwort.ok || d.error) throw new Error(d.error || "HTTP " + antwort.status);
return d;
}
async function skodaHolen() {
const ziel = document.getElementById("skodaListe");
if (!ziel) return;
try {
skodaModell = await skodaFrage("skoda-keys");
zeichneSkoda();
} catch (e) {
ziel.innerHTML = '<div class="alert alert-danger mb-0">Schlüssel nicht abrufbar: '
+ textSchuetzen(e.message) + "</div>";
}
}
/**
* Alles Eingetragene auf einmal senden.
*
* Leere Felder bedeuten "so lassen" - sonst löschte ein Speichern ohne
* Eingabe alles, was man nicht noch einmal eintippt.
*
* Das kann dauern: der Server probiert jeden neuen Schlüssel einmal an der
* API aus, bevor er ihn hinschreibt. Deshalb der Knopf mit Wartezustand.
*/
async function skodaSenden() {
const knopf = document.getElementById("skodaSpeichern");
const felder = {};
document.querySelectorAll("#skodaListe .skoda-feld").forEach(function (zeile) {
const feld = zeile.dataset.feld;
const wert = zeile.querySelector("input").value.trim();
if (wert) felder[feld] = wert;
});
if (!Object.keys(felder).length) {
skodaStand("Kein Schlüssel eingetragen.", "text-warning");
return;
}
if (knopf) knopf.disabled = true;
skodaStand("Wird an der Škoda-API ausprobiert …");
try {
const d = await skodaFrage("skoda-keys-save", { felder: felder });
skodaModell = d.stand;
zeichneSkoda();
skodaStand((d.meldungen || []).join(" ") || "Eingetragen.", "text-success");
} catch (e) {
skodaStand(e.message, "text-danger");
if (knopf) knopf.disabled = false;
}
}
/**
* Einen Schlüssel entfernen.
*
* Mit Rückfrage: er steht danach nirgends mehr, und ein neuer kommt nur aus
* der MyŠkoda-App.
*/
async function skodaLoeschen(feld) {
if (!window.confirm(feld + " entfernen?\n\nDer Schlüssel verschwindet aus der "
+ "Konfiguration. Zurückholen lässt er sich nur über die MyŠkoda-App.")) {
return;
}
skodaStand("Wird entfernt …");
try {
const d = await skodaFrage("skoda-keys-save", { loeschen: [feld] });
skodaModell = d.stand;
zeichneSkoda();
skodaStand(feld + " entfernt.", "text-success");
} catch (e) {
skodaStand(e.message, "text-danger");
}
}
/* --- Bedienung ----------------------------------------------------------- */
function skodaBinden() {
const liste = document.getElementById("skodaListe");
if (!liste) return;
liste.addEventListener("click", function (ereignis) {
const zeigen = ereignis.target.closest(".skoda-zeigen");
if (zeigen) {
// Ein eingefügter Schlüssel ist nicht zu merken, nur zu vergleichen -
// wer ihn nicht sehen kann, sucht einen Tippfehler im Dunkeln.
const feld = zeigen.closest(".input-group").querySelector("input");
feld.type = feld.type === "password" ? "text" : "password";
zeigen.querySelector("i").className =
feld.type === "password" ? "bi bi-eye" : "bi bi-eye-slash";
return;
}
const weg = ereignis.target.closest(".skoda-weg");
if (weg) {
skodaLoeschen(weg.dataset.feld);
return;
}
if (ereignis.target.closest("#skodaSpeichern")) {
skodaSenden();
}
});
// Enter im Feld soll dasselbe tun wie der Knopf daneben.
liste.addEventListener("keydown", function (ereignis) {
if (ereignis.key === "Enter" && ereignis.target.matches(".skoda-feld input")) {
ereignis.preventDefault();
skodaSenden();
}
});
const neuLaden = document.getElementById("skodaNeuLaden");
if (neuLaden) neuLaden.addEventListener("click", skodaHolen);
}
document.addEventListener("DOMContentLoaded", function () {
skodaBinden();
skodaHolen();
});