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:
+269
-8
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user