Ladefenster neu: Zustand zuerst, Modus als Karten, Aenderungen sofort

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>
This commit is contained in:
2026-09-15 20:40:29 +02:00
co-authored by Claude Opus 5
parent 2e66bdeff5
commit b0bac3d3ba
9 changed files with 564 additions and 237 deletions
+2 -2
View File
@@ -183,7 +183,7 @@ genau die Topics, die auf den Kacheln stehen (`tileTopics()` in `rooms.php`).
| Seite (`?action=`) | Vorlage | Seitenskripte | holt per fetch | hört auf MQTT |
|---|---|---|---|---|
| `solar` (Vorgabe) | `solar.php` | `jahresstatistik.js`, `anlageKatalog.js`, `anlage.js`, `speicher.js`, `energieflussKatalog.js`, `energiefluss.js`, `solarMQTT.js` | `speicher`, `getProdData`, `getConsData`, `getForecastData`, `getSunrise`, `getStats`, `carEG`, `carOG`, `heater`, `watering` | `solarManager/#`, `weatherStation/#`, `wattpilot/#`, `go-eCharger/#`, `Gartenwasser/#` |
| `solar` (Vorgabe) | `solar.php` | `jahresstatistik.js`, `anlageKatalog.js`, `anlage.js`, `speicher.js`, `energieflussKatalog.js`, `energiefluss.js`, `bewaesserungAnzeige.js`, `ladefenster.js`, `solarMQTT.js` | `speicher`, `getProdData`, `getConsData`, `getForecastData`, `getSunrise`, `getStats`, `carEG`, `carOG`, `heater`, `watering` | `solarManager/#`, `weatherStation/#`, `wattpilot/#`, `go-eCharger/#`, `Gartenwasser/#` |
| `home` | `home.php` | `autoActionFuncs.js`, `zeitleiste.js`, `homeMQTT.js` | `room.php`, `AutoAction.php` | die Topics der Kacheln, `Raumtemp/#` |
| `heat` | `heat.php` | `heatMQTT.js` | `getHeaterData`, `getWaterData`, `getSunrise` | `solarManager/#`, `weatherStation/#`, Wallbox-Topics |
| `history` | `history.php` | `jahresstatistik.js`, `historyMQTT.js` | `energyHistory` (6 ×), `getStats` | — |
@@ -433,7 +433,7 @@ klein: Diagramme füllen bzw. Logzeilen nachladen und einfärben.
| `skodaCmd.php` | `POST befehl=…` | Laden/Klima/Lüftung am Fahrzeug |
| `tile.php` | `?z=&x=&y=` | Kartenkachel aus dem eigenen Zwischenspeicher, sonst einmalig von OSM |
| `logs.php` | `?action=list\|read` | Zustand und Zeilen der Logdateien |
| `carEG.php` `carOG.php` `carSteuerung.php` `carForm.php` `heater.php` `watering.php` `tahoma.php` | | Wallboxen, Heizstab, Bewässerung, Tahoma-Bedienung |
| `carEG.php` `carOG.php` `carSteuerung.php` `carForm.php` `heater.php` `watering.php` `tahoma.php` | | Wallboxen, Heizstab, Bewässerung, Tahoma-Bedienung. Das Ladefenster (`carForm.php` + `js/solar/ladefenster.js`) liest live aus MQTT und schreibt jede Änderung sofort als JSON an `carSteuerung.php` (Aktionen `frc`, `modus`, `strom`, `plan`) |
| `chartData.php` | (kein Endpunkt) | gemeinsame Bausteine der Chart-Endpunkte |
| `phpMQTT.php` | (Bibliothek) | MQTT aus PHP heraus, für Kommandos |
+76 -77
View File
@@ -1,94 +1,93 @@
<?php
/*
* Formular des Auto-Lademodals. carEG.php und carOG.php zeigen dasselbe
* Formular; nur die Wallbox dahinter unterscheidet sich - eingebunden wird
* es aus carSteuerung.php, das $wallbox und $formId setzt. $formId trennt
* die beiden Formulare fuer das JavaScript.
* Das Ladefenster, gemeinsam für beide Wallboxen (carEG.php, carOG.php über
* carSteuerung.php).
*
* Der Schieberegler traegt die Wattstunden je Prozent als data-Attribut bei
* sich. Frueher stand die Umrechnung als feste 14000 im JavaScript und
* passte damit hoechstens zu einem der beiden Autos: eingestellte und
* angezeigte Prozente gingen auseinander.
* Hier steht nur das Gerüst mit dem, was der Server weiß: welches Auto, wie
* groß sein Akku ist. Werte, Zustände und jede Änderung übernimmt
* js/solar/ladefenster.js - live aus den MQTT-Werten der Seite, geschrieben
* wird sofort per POST an carSteuerung.php.
*/
$auto = fahrzeuge()[$wallbox];
$whProProzent = wallboxWhProProzent($wallbox);
$box = wallboxen()[$wallbox];
?>
<div class="d-grid gap-2 col-6 mx-auto my-4">
<button class="btn btn-primary" id="evStart/Stop">Laden starten</button>
<div class="lade" data-wallbox="<?= htmlspecialchars($wallbox) ?>"
data-wh-pro-prozent="<?= wallboxWhProProzent($wallbox) ?>"
data-akku-kwh="<?= htmlspecialchars((string)$auto["akkuKWh"]) ?>">
<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-lade="soc"></b>
<span data-lade="ringUnten"></span>
</div>
<!--begin::Form-->
<form id="<?= $formId ?>">
<div class="card m-3">
<div class="card-header py-1 px-3">
<div class="card-title">Modus</div>
</div>
<!--begin::Body-->
<div class="card-body">
<div class="col-sm-10">
<div class="form-check">
<input class="form-check-input" type="radio" name="evMode" id="EV_Default" value="default">
<label class="form-check-label" for="EV_Default"> Immer Laden </label></input>
<div class="lade-text">
<div class="lade-auto"><?= htmlspecialchars($auto["name"]) ?> <span data-lade="stecker" class="lade-pille"></span></div>
<div class="lade-gross" data-lade="leistung"></div>
<div class="lade-zeile" data-lade="quelle"></div>
<div class="lade-zeile" data-lade="fertig"></div>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="evMode" id="EV_Eco" value="eco">
<label class="form-check-label" for="EV_Eco"> Überschuss </label></input>
</div>
<div class="form-check disabled">
<input class="form-check-input" type="radio" name="evMode" id="EV_NextTrip" value="NextTrip">
<label class="form-check-label" for="EV_NextTrip"> Ladeplanung </label></input>
<button type="button" class="lade-start" data-lade="start"><span class="lade-punkt"></span><span data-lade="startText"></span></button>
<div class="lade-erklaerung" data-lade="erklaerung"></div>
</div>
<div class="lade-rechts">
<div class="lade-abschnitt">
<h3 id="lade-modus-<?= $wallbox ?>">Wie laden?</h3>
<div class="lade-modi" role="radiogroup" aria-labelledby="lade-modus-<?= $wallbox ?>">
<button type="button" class="lade-modus" role="radio" aria-checked="false" data-modus="sofort" style="--c:#6cbe58">
<span class="lade-sym"><svg viewBox="0 0 24 24"><path d="M13 3 5 14h6l-1 7 8-11h-6z"/></svg></span>
<b>Sofort</b><span>volle Leistung, auch aus dem Netz</span>
</button>
<button type="button" class="lade-modus" role="radio" aria-checked="false" data-modus="sonne" style="--c:#f7c000">
<span class="lade-sym"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg></span>
<b>Sonne</b><span>nur Überschuss der Anlage</span>
</button>
<button type="button" class="lade-modus" role="radio" aria-checked="false" data-modus="plan" style="--c:#4b9fd6">
<span class="lade-sym"><svg viewBox="0 0 24 24"><circle cx="12" cy="13" r="8"/><path d="M12 9v4l2.5 2M9 2h6"/></svg></span>
<b>Geplant</b><span>bis zur Abfahrt geladen</span>
</button>
</div>
</div>
<div class="lade-planung" data-lade="planung" hidden>
<div>
<div class="lade-plan-zeile">
<label for="lade-ziel-<?= $wallbox ?>" data-lade="zielTitel">Ladestand bei Abfahrt</label>
<output data-lade="zielAusgabe"></output>
</div>
<input type="range" id="lade-ziel-<?= $wallbox ?>" data-lade="ziel" min="0" max="100" step="5" value="80">
<div class="lade-marken"><span data-lade="markeLinks">0 %</span><span data-lade="markeMitte"></span><span>100 %</span></div>
</div>
<div>
<div class="lade-plan-zeile"><label>Abfahrt</label><output data-lade="abfahrtAusgabe"></output></div>
<div class="lade-chips" role="group" aria-label="Abfahrt">
<button type="button" class="lade-chip" aria-pressed="false" data-zeit="06:00">06:00</button>
<button type="button" class="lade-chip" aria-pressed="false" data-zeit="07:00">07:00</button>
<button type="button" class="lade-chip" aria-pressed="false" data-zeit="08:00">08:00</button>
<label class="lade-chip" aria-pressed="false" data-lade="chipEigen">andere
<input type="time" data-lade="zeitEigen" aria-label="eigene Abfahrtszeit"></label>
</div>
</div>
</div>
</div>
<div class="card m-3">
<div class="card-header py-1 px-3">
<div class="card-title">Max. Ladestrom</div>
</div>
<div class="card-body">
<div class="col-sm-10">
<div class="form-check">
<input class="form-check-input" type="radio" name="evAmp" id="EV_6A" value="6">
<label class="form-check-label" for="EV_6A"> 6A </label></input>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="evAmp" id="EV_10A" value="10">
<label class="form-check-label" for="EV_10A"> 10A </label></input>
</div>
<div class="form-check disabled">
<input class="form-check-input" type="radio" name="evAmp" id="EV_16A" value="16">
<label class="form-check-label" for="EV_16A"> 16A </label></input>
<div class="lade-abschnitt">
<h3 id="lade-strom-<?= $wallbox ?>">Höchstens</h3>
<div class="lade-strom" role="radiogroup" aria-labelledby="lade-strom-<?= $wallbox ?>">
<button type="button" role="radio" aria-checked="false" data-ampere="6"><b>6 A</b><span> 4,1 kW</span></button>
<button type="button" role="radio" aria-checked="false" data-ampere="10"><b>10 A</b><span> 6,9 kW</span></button>
<button type="button" role="radio" aria-checked="false" data-ampere="16"><b>16 A</b><span> 11 kW</span></button>
</div>
</div>
<div class="lade-meldung" data-lade="meldung" aria-live="polite">Änderungen gelten sofort · <?= htmlspecialchars($box["titel"]) ?></div>
</div>
</div>
<div class="card m-3">
<div class="card-header py-1 px-3">
<div class="card-title">Ladeplanung</div>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="form-check">
<label class="form-label" for="addedCharge">Zusätzlich benötigte Ladung (%): </label>
<div style="font-size: 15px;color: #297195;display: inline-block;" id="modal-slider-label">sdf</div>
<input type="range" id="modal-slider" name="fte" class="form-range range-color-track" min="1" max="100"
data-wh-pro-prozent="<?= $whProProzent ?>" />
<div class="small text-secondary">
<?= htmlspecialchars($auto["name"]) ?>,
<?= htmlspecialchars(rtrim(rtrim(number_format($auto["akkuKWh"], 1, ",", "."), "0"), ",")) ?> kWh nutzbar
</div>
<div class="d-flex justify-content-between">
<span class="max-amount f-16 font-weight-normal darkGray-color text-right mt-1">1%</span>
<span class="max-amount f-16 font-weight-normal darkGray-color text-right mt-1">100%</span>
</div>
<!--<input name="fte" class="form-control" type="number" max="100" min="1" value="" />-->
</div>
</div>
<div class="row mb-3">
<div class="form-check">
<label class="form-label" for="planedTime">Zeitpunkt zudem geladen sein soll: </label>
<input id="EV_NextTripTime" name="ftt" class="form-control" size="2" type="time" value=""/>
</div>
</div>
</div>
</div>
</form>
+82 -61
View File
@@ -2,86 +2,107 @@
/**
* Ladesteuerung beider Wallboxen.
*
* carEG.php und carOG.php setzen nur $wallbox und $formId und binden diese
* Datei ein - vorher standen hier zwei fast gleiche Dateien, die sich nur im
* Transportweg unterschieden. Was die beiden Wallboxen wirklich trennt,
* steht in restricted/wallboxen.php.
* carEG.php und carOG.php setzen nur $wallbox und binden diese Datei ein.
* Was die beiden Wallboxen wirklich trennt, steht in restricted/wallboxen.php.
*
* Ohne POST wird das Formular ausgeliefert (carForm.php), mit POST wird
* geschaltet und nichts zurueckgegeben - so war es vorher auch, das
* JavaScript im Modal erwartet es so.
* GET das Ladefenster (carForm.php)
* POST {"aktion": "frc", "wert": 0|1|2} Freigabe: 0 Modus entscheidet, 1 gesperrt, 2 sofort
* POST {"aktion": "modus", "modus": "sofort"|"sonne"}
* POST {"aktion": "strom", "ampere": 6..16}
* POST {"aktion": "plan", "wh": 1.., "zeit": "HH:MM"} Ladeplanung: Energie bis Uhrzeit
*
* Seit September 2026 gilt jede Änderung sofort (js/solar/ladefenster.js) -
* vorher gingen Modus, Strom und Planung gemeinsam über "Save changes".
* Antwort ist JSON: {"ok": true} oder {"error": "..."}. Ob die Wallbox den
* Wert genommen hat, meldet sie über MQTT; das Fenster zeigt es beim
* nächsten Takt.
*/
require_once(__DIR__ . "/../helper.php");
require_once(__DIR__ . "/../restricted/wallboxen.php");
// lmo, der Betriebsmodus der Wallbox. Die Namen links kommen aus den
// Radiobuttons in carForm.php.
// lmo, der Betriebsmodus - beide Wallboxen nutzen dieselben Nummern.
const CAR_MODI = [
"default" => 3, // Immer laden
"eco" => 4, // Ueberschuss (Awattar)
"NextTrip" => 5, // Ladeplanung (AutomaticStop)
"sofort" => 3, // Default: volle Leistung, auch aus dem Netz
"sonne" => 4, // Eco (Awattar): Überschuss
"plan" => 5, // Next Trip (AutomaticStop): Energie bis Uhrzeit
];
$close = 0;
function ladeAntwort($daten, $code = 200)
{
http_response_code($code);
header("Content-Type: application/json; charset=utf-8");
echo json_encode($daten, JSON_UNESCAPED_UNICODE);
exit;
}
if (!checkLogin()) {
if ($_SERVER["REQUEST_METHOD"] === "POST") {
ladeAntwort(["error" => "Nicht angemeldet."], 403);
}
exit;
}
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
include __DIR__ . "/carForm.php";
exit;
}
if (checkLogin()) {
$konf = wallboxen()[$wallbox];
if (isset($_POST["evStart/Stop"])) {
// frc: 2 gibt das Laden frei, 1 sperrt es, 0 waere "die Wallbox
// entscheidet".
if ($_POST["evStart/Stop"] === "Laden starten") {
wallboxSetzen($wallbox, ["frc" => 2]);
} elseif ($_POST["evStart/Stop"] === "Laden stoppen") {
wallboxSetzen($wallbox, ["frc" => 1]);
}
$close = 1;
} elseif (isset($_POST["evMode"])) {
$modus = CAR_MODI[$_POST["evMode"]] ?? null;
if ($modus !== null) {
// Der Schieberegler liefert Prozent zusaetzlich benoetigter
// Ladung; die Wallbox will Wattstunden. Der Faktor haengt am
// Auto und steht deshalb in restricted/config.php.
$prozent = min(100, max(0, intval($_POST["fte"] ?? 0)));
$energie = $prozent * wallboxWhProProzent($wallbox);
$strom = min(16, max(6, intval($_POST["evAmp"] ?? 6)));
// Das Formularfeld ist ein <input type="time">, liefert also
// HH:MM. Alles andere gilt als "keine Ladeplanung" und laesst
// Zielzeit und Zielenergie unangetastet. Geprueft wird hier
// weiterhin streng: der Wert ging frueher in eine Kommandozeile
// und liess sich mit "12:00; befehl" missbrauchen.
$sekunden = null;
if (isset($_POST["ftt"])
&& preg_match('/^([01][0-9]|2[0-3]):([0-5][0-9])$/', $_POST["ftt"], $teile)) {
$sekunden = intval($teile[1]) * 3600 + intval($teile[2]) * 60;
$body = json_decode(file_get_contents("php://input"), true);
if (!is_array($body)) {
ladeAntwort(["error" => "Konnte die Anfrage nicht lesen."], 400);
}
// Reihenfolge zaehlt: Ziel zuerst, Modus zuletzt.
switch ($body["aktion"] ?? "") {
case "frc":
$wert = intval($body["wert"] ?? -1);
if (!in_array($wert, [0, 1, 2], true)) {
ladeAntwort(["error" => "Unbekannte Freigabe."], 400);
}
$werte = ["frc" => $wert];
break;
case "modus":
$modus = CAR_MODI[$body["modus"] ?? ""] ?? null;
if ($modus === null || $modus === CAR_MODI["plan"]) {
// Die Planung braucht Energie und Uhrzeit - siehe "plan".
ladeAntwort(["error" => "Unbekannter Modus."], 400);
}
$werte = ["fup" => true, "lmo" => $modus];
break;
case "strom":
$werte = ["amp" => min(16, max(6, intval($body["ampere"] ?? 6)))];
break;
case "plan":
// Die Uhrzeit ging früher in eine Kommandozeile und ließ sich mit
// "12:00; befehl" missbrauchen - deshalb bleibt die Prüfung streng.
if (!preg_match('/^([01][0-9]|2[0-3]):([0-5][0-9])$/', (string)($body["zeit"] ?? ""), $teile)) {
ladeAntwort(["error" => "Die Uhrzeit ist ungültig."], 400);
}
$wh = intval($body["wh"] ?? 0);
$max = (int) round(fahrzeuge()[$wallbox]["akkuKWh"] * 1000);
if ($wh < 100 || $wh > $max) {
ladeAntwort(["error" => "Die Energiemenge liegt außerhalb dessen, was der Akku fasst."], 400);
}
// Reihenfolge zählt: Uhr, Ziel, dann der Modus.
$werte = [];
if (!empty($konf["uhrStellen"])) {
$werte["utc"] = gmdate("Y-m-d\TH:i:s") . ".000";
}
if ($sekunden !== null && $energie > 0) {
$werte["fte"] = $energie;
$werte["ftt"] = $sekunden;
}
$werte["amp"] = $strom;
$werte["fte"] = $wh;
$werte["ftt"] = intval($teile[1]) * 3600 + intval($teile[2]) * 60;
$werte["fup"] = true;
$werte["lmo"] = $modus;
$werte["lmo"] = CAR_MODI["plan"];
break;
wallboxSetzen($wallbox, $werte);
}
$close = 1;
}
} else {
$close = 1;
default:
ladeAntwort(["error" => "Unbekannte Aktion."], 400);
}
if (!$close) {
include __DIR__ . "/carForm.php";
if (!wallboxSetzen($wallbox, $werte)) {
ladeAntwort(["error" => "Der MQTT-Broker ist nicht erreichbar."], 502);
}
ladeAntwort(["ok" => true]);
+105
View File
@@ -3949,6 +3949,111 @@ body.anzeige-ohne-maus * { cursor: none !important; }
.anzeige-ruhig .enf.enf-pause .enf-strom { animation: none !important; }
.anzeige-ruhig .enf .enf-koerper { transition: none; }
/* ==========================================================================
Ladefenster der Wallboxen (ajax/carForm.php, js/solar/ladefenster.js).
Oben der Zustand, dann ein Knopf, darunter Modus, Planung, Strom.
========================================================================== */
.lade { display: grid; gap: 1rem; padding: 1rem 1.1rem 1.1rem; --lade-wallbox: #60afbf; }
.lade .lade-links,
.lade .lade-rechts { display: grid; gap: .9rem; align-content: start; min-width: 0; }
.lade .lade-status { display: grid; grid-template-columns: 7.5rem minmax(0, 1fr); gap: .9rem; align-items: center; }
.lade .lade-ring { position: relative; width: 7.5rem; height: 7.5rem; }
.lade .lade-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.lade .lade-spur { fill: none; stroke: #2b3237; stroke-width: 12; }
.lade .lade-ist { fill: none; stroke: #6cbe58; stroke-width: 12; stroke-linecap: round; stroke-dasharray: 0 100; transition: stroke-dasharray .7s cubic-bezier(.2, .7, .2, 1); }
.lade .lade-ziel { fill: none; stroke: var(--lade-wallbox); stroke-width: 12; opacity: .45; stroke-dasharray: 0 100; transition: stroke-dasharray .7s, stroke-dashoffset .7s; }
.lade .lade-ring-mitte { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; }
.lade .lade-ring-mitte b { font-size: 1.55rem; font-weight: 600; line-height: 1; font-variant-numeric: tabular-nums; }
.lade .lade-ring-mitte span { font-size: .7rem; color: var(--lade-wallbox); font-weight: 600; margin-top: .25rem; }
.lade .lade-text { display: grid; gap: .25rem; min-width: 0; }
.lade .lade-auto { font-size: .8rem; color: var(--ton-schrift-2, #9aa6ad); display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }
.lade .lade-pille { font-size: .68rem; font-weight: 600; border-radius: 99px; padding: .05rem .5rem; background: rgba(255, 255, 255, .06); color: var(--ton-schrift-2, #9aa6ad); }
.lade .lade-pille:empty { display: none; }
.lade .lade-pille.an { color: #6cbe58; background: rgba(108, 190, 88, .14); }
.lade .lade-gross { font-size: 1.45rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.lade .lade-gross small { font-size: .8rem; font-weight: 400; color: var(--ton-schrift-2, #9aa6ad); margin-left: .35rem; }
.lade .lade-zeile { font-size: .82rem; color: var(--ton-schrift-2, #9aa6ad); }
.lade .lade-zeile:empty { display: none; }
.lade .lade-start {
all: unset; box-sizing: border-box; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: .6rem;
height: 3.1rem; border-radius: .9rem; font-weight: 600; font-size: 1rem;
background: linear-gradient(180deg, #6cbe58, #57a646); color: #0f1a0c; box-shadow: 0 .35rem 1rem rgba(108, 190, 88, .22);
transition: transform .15s, filter .2s;
}
.lade .lade-start:hover { filter: brightness(1.06); }
.lade .lade-start:active { transform: scale(.98); }
.lade .lade-start:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.lade .lade-start.laeuft { background: linear-gradient(180deg, #3a4147, #30363b); color: var(--ton-schrift, #e8ede9); box-shadow: none; border: 1px solid var(--ton-linie, rgba(138, 148, 158, .2)); }
.lade .lade-start.aus { background: #2b3237; color: var(--ton-schrift-3, #6f7a81); box-shadow: none; cursor: default; }
.lade .lade-start.aus .lade-punkt { display: none; }
.lade .lade-start.gesperrt { background: linear-gradient(180deg, #e8a33a, #cf8f2c); color: #1d1406; box-shadow: 0 .35rem 1rem rgba(232, 163, 58, .2); }
.lade .lade-punkt { width: .55rem; height: .55rem; border-radius: 50%; background: currentColor; }
.lade .lade-start.laeuft .lade-punkt { background: #6cbe58; animation: lade-puls 1.6s infinite; }
@keyframes lade-puls { 0% { box-shadow: 0 0 0 0 rgba(108, 190, 88, .6); } 70%, 100% { box-shadow: 0 0 0 .5rem rgba(108, 190, 88, 0); } }
.lade .lade-erklaerung { padding: .5rem .7rem; border-radius: .6rem; background: rgba(96, 175, 191, .1); color: #bfe3ea; font-size: .8rem; }
.lade .lade-erklaerung:empty { display: none; }
.lade .lade-abschnitt { display: grid; gap: .45rem; }
.lade h3 { margin: 0; font-size: .7rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--ton-schrift-2, #9aa6ad); }
.lade .lade-modi { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .45rem; }
.lade .lade-modus {
all: unset; box-sizing: border-box; cursor: pointer; display: grid; gap: .2rem; justify-items: start; padding: .6rem .65rem;
border-radius: .75rem; background: var(--ton-flaeche-2, #2b3237); border: 1.5px solid transparent; transition: border-color .2s, background .2s;
}
.lade .lade-modus:hover { background: #333b41; }
.lade .lade-modus:focus-visible { outline: 2px solid var(--lade-wallbox); outline-offset: 1px; }
.lade .lade-modus b { font-weight: 600; font-size: .88rem; }
.lade .lade-modus > span:last-child { font-size: .7rem; color: var(--ton-schrift-2, #9aa6ad); line-height: 1.3; }
.lade .lade-sym { width: 1.7rem; height: 1.7rem; border-radius: .5rem; display: grid; place-items: center; background: rgba(255, 255, 255, .06); }
.lade .lade-sym svg { width: 1.1rem; height: 1.1rem; stroke: var(--c); fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.lade .lade-modus[aria-checked="true"] { border-color: var(--c); background: color-mix(in srgb, var(--c) 12%, var(--ton-flaeche-2, #2b3237)); }
.lade .lade-modus[aria-checked="true"] .lade-sym { background: color-mix(in srgb, var(--c) 25%, transparent); }
.lade .lade-planung { display: grid; gap: .7rem; padding: .7rem .75rem; border-radius: .75rem; background: var(--ton-flaeche-2, #2b3237); animation: lade-auf .3s cubic-bezier(.2, .7, .2, 1); }
.lade .lade-planung[hidden] { display: none; }
@keyframes lade-auf { from { opacity: 0; transform: translateY(-.35rem); } }
.lade .lade-plan-zeile { display: flex; justify-content: space-between; align-items: baseline; gap: .5rem; }
.lade .lade-plan-zeile label { font-size: .82rem; color: var(--ton-schrift-2, #9aa6ad); margin: 0; }
.lade .lade-plan-zeile output { font-weight: 600; font-variant-numeric: tabular-nums; }
.lade .lade-plan-zeile output small { color: var(--ton-schrift-2, #9aa6ad); font-weight: 400; margin-left: .3rem; }
.lade input[type="range"] { -webkit-appearance: none; appearance: none; width: 100%; height: 1.9rem; background: transparent; margin: 0; }
.lade input[type="range"]::-webkit-slider-runnable-track { height: .5rem; border-radius: .25rem; background: linear-gradient(90deg, #6cbe58 0 var(--ist, 0%), var(--lade-wallbox) var(--ist, 0%) var(--wert, 0%), #394046 var(--wert, 0%)); }
.lade input[type="range"]::-moz-range-track { height: .5rem; border-radius: .25rem; background: linear-gradient(90deg, #6cbe58 0 var(--ist, 0%), var(--lade-wallbox) var(--ist, 0%) var(--wert, 0%), #394046 var(--wert, 0%)); }
.lade input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 1.6rem; height: 1.6rem; margin-top: -.55rem; border-radius: 50%; background: #fff; border: 4px solid var(--lade-wallbox); box-shadow: 0 .15rem .5rem rgba(0, 0, 0, .4); }
.lade input[type="range"]::-moz-range-thumb { width: 1.1rem; height: 1.1rem; border-radius: 50%; background: #fff; border: 4px solid var(--lade-wallbox); }
.lade input[type="range"]:focus-visible { outline: 2px solid var(--lade-wallbox); outline-offset: 4px; }
.lade .lade-marken { display: flex; justify-content: space-between; font-size: .68rem; color: var(--ton-schrift-3, #6f7a81); margin-top: -.2rem; }
.lade .lade-chips { display: flex; flex-wrap: wrap; gap: .35rem; }
.lade .lade-chip { all: unset; cursor: pointer; padding: .35rem .7rem; border-radius: 99px; background: #363d43; font-size: .8rem; font-weight: 500; font-variant-numeric: tabular-nums; }
.lade .lade-chip[aria-pressed="true"] { background: var(--lade-wallbox); color: #0c1a1d; font-weight: 600; }
.lade .lade-chip:focus-visible { outline: 2px solid var(--lade-wallbox); outline-offset: 1px; }
.lade .lade-chip input { all: unset; width: 4.6ch; font: inherit; color: inherit; }
.lade .lade-strom { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .3rem; padding: .25rem; border-radius: .75rem; background: var(--ton-flaeche-2, #2b3237); }
.lade .lade-strom button { all: unset; cursor: pointer; text-align: center; padding: .45rem .2rem; border-radius: .55rem; display: grid; gap: .05rem; }
.lade .lade-strom button b { font-weight: 600; }
.lade .lade-strom button span { font-size: .68rem; color: var(--ton-schrift-2, #9aa6ad); }
.lade .lade-strom button[aria-checked="true"] { background: #363d43; box-shadow: inset 0 0 0 1.5px var(--lade-wallbox); }
.lade .lade-strom button:focus-visible { outline: 2px solid var(--lade-wallbox); }
.lade .lade-meldung { font-size: .76rem; color: var(--ton-schrift-3, #6f7a81); }
.lade .lade-meldung.gut { color: #6cbe58; }
.lade .lade-meldung.fehler { color: #e5624f; }
@media (max-width: 420px) { .lade .lade-modus > span:last-child { display: none; } }
/* Am 800er-Display zweispaltig ohne Scrollen. */
.anzeige #modalEV .modal-dialog:has(.lade) { max-width: 46rem; }
.anzeige .lade { grid-template-columns: 15rem minmax(0, 1fr); gap: 1rem; padding: .7rem .9rem; }
.anzeige .lade .lade-status { grid-template-columns: 1fr; justify-items: center; text-align: center; gap: .5rem; }
.anzeige .lade .lade-auto { justify-content: center; }
.anzeige .lade .lade-modus > span:last-child { display: none; }
.anzeige .lade .lade-rechts,
.anzeige .lade .lade-links { gap: .6rem; }
.anzeige #modalEV .modal-body:has(.lade) { max-height: none; overflow: visible; }
.anzeige #modalEV .modal-content:has(.lade) .modal-footer { display: none; }
.anzeige #modalEV .modal-content:has(.lade) .modal-header { padding: .45rem .9rem; }
/* --- Karte: Umschalter Übersicht / Anlage -------------------------------- */
.enf-umschalter { display: inline-flex; gap: 2px; padding: 3px; border-radius: var(--ton-radius-klein, .5rem); background: var(--ton-grund, #191d20); }
.enf-umschalter .btn.btn-sm { border: 0; color: var(--ton-schrift-2, #9aa6ad); font-weight: 600; padding: .2rem .8rem; }
+1 -1
View File
@@ -101,7 +101,7 @@ if (isset($_GET["addUser"])) {
$pages = [
"solar" => ["template" => "solar.php", "mqtt" => true, "charts" => true, "meteogram" => true, "leaflet" => false,
"scripts" => ["js/solar/jahresstatistik.js", "js/solar/anlageKatalog.js", "js/solar/anlage.js", "js/solar/speicher.js",
"js/solar/energieflussKatalog.js", "js/solar/energiefluss.js", "js/solar/bewaesserungAnzeige.js", "js/solar/solarMQTT.js"]],
"js/solar/energieflussKatalog.js", "js/solar/energiefluss.js", "js/solar/bewaesserungAnzeige.js", "js/solar/ladefenster.js", "js/solar/solarMQTT.js"]],
"home" => ["template" => "home.php", "mqtt" => true, "charts" => false, "meteogram" => false, "leaflet" => false,
"scripts" => ["js/solar/autoActionFuncs.js", "js/solar/zeitleiste.js", "js/solar/homeMQTT.js"]],
"heat" => ["template" => "heat.php", "mqtt" => true, "charts" => true, "meteogram" => false, "leaflet" => false,
+2 -6
View File
@@ -74,8 +74,8 @@ async function getData(chart, url, options = {}) {
}
/**
* Formular des Modals per POST abschicken.
* Der Start/Stop-Knopf der Ladesteuerung schickt nur sich selbst.
* 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();
@@ -90,9 +90,6 @@ function submitFormAjax(event) {
};
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;
@@ -108,7 +105,6 @@ function submitFormAjax(event) {
post += element.name + "=" + encodeURIComponent(element.value) + "&";
}
}
}
xmlhttp.open("POST", event.currentTarget.contentURL, true);
xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
+4 -2
View File
@@ -271,8 +271,10 @@ const ENERGIEFLUSS_KATALOG = (function () {
const roh = m.text("solarManager/evPlug");
const stecker = isNaN(Number(roh)) ? (roh !== undefined && roh !== "no car") : Number(roh) > 0;
let neben = Math.round(soc) + " %";
const rest = m.zahl("solarManager/carRemChrg");
if (w >= 50 && rest > -1 && m.text("solarManager/carRemChrg") !== undefined) {
// Restladezeit in Minuten laut Fahrzeug (gatherSkodaData.py). Das
// frühere carRemChrg schickt der Manager nicht mehr.
const rest = m.zahl("solarManager/evRemChrgTime");
if (w >= 50 && rest > 0) {
const fertig = new Date(Date.now() + rest * 60000);
neben = "fertig " + String(fertig.getHours()).padStart(2, "0") + ":" + String(fertig.getMinutes()).padStart(2, "0");
}
+259
View File
@@ -0,0 +1,259 @@
/* ==========================================================================
Ladefenster der Wallboxen (Gerüst: ajax/carForm.php, Schreiben:
ajax/carSteuerung.php).
ladefensterStarten(wurzel, url) nach dem Laden des Fensters
Liest jede Sekunde aus mqttData (von der Seite gefüllt) und schreibt jede
Änderung sofort. Was eben geändert wurde, gilt HALTEN ms lang lokal - so
springt die Anzeige nicht auf den alten Stand zurück, bevor die Wallbox
den neuen meldet.
Freigabe (frc) wird gelesen, nicht geraten: 0 = der Modus entscheidet,
1 = gesperrt, 2 = lädt sofort. Der große Knopf bietet jeweils den
sinnvollen nächsten Schritt an.
========================================================================== */
(function () {
"use strict";
const HALTEN = 8000;
// Was die Wallboxen melden und wie es heißt. Geschrieben wird mit denselben
// Schlüsseln über restricted/wallboxen.php.
const WALLBOXEN = {
eg: { // Wattpilot, über wattpilot_bruecke.py im SolarManager
modus: "wattpilot/properties/lmo/state", modi: { Default: "sofort", Awattar: "sonne", AutomaticStop: "plan", 3: "sofort", 4: "sonne", 5: "plan" },
ampere: "wattpilot/properties/amp/state",
energie: "wattpilot/properties/fte/state", zeit: "wattpilot/properties/ftt/state",
frc: "wattpilot/properties/frc/state", frcWerte: { Neutral: 0, Off: 1, On: 2, 0: 0, 1: 1, 2: 2 },
auto: "wattpilot/properties/car/state", autoWerte: { Idle: "kein", Charging: "laedt", WaitCar: "wartet", Complete: "fertig" },
leistungKW: "solarManager/evPower", soc: "solarManager/evSOC", restMin: "solarManager/evRemChrgTime",
},
og: { // go-e Charger, direkt am Broker
modus: "go-eCharger/270003/lmo", modi: { 3: "sofort", 4: "sonne", 5: "plan" },
ampere: "go-eCharger/270003/amp",
energie: "go-eCharger/270003/ate", zeit: "go-eCharger/270003/att",
frc: "go-eCharger/270003/frc", frcWerte: { 0: 0, 1: 1, 2: 2 },
auto: "go-eCharger/270003/car", autoWerte: { 1: "kein", 2: "laedt", 3: "wartet", 4: "fertig" },
leistungKW: "solarManager/evPowerOG", soc: null, restMin: null,
},
};
const KW_JE_A = { 6: 4.1, 10: 6.9, 16: 11 }; // dreiphasig, 230 V
const komma = (z, n = 1) => Number(z).toFixed(n).replace(".", ",");
const hhmm = d => String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
const text = t => { const v = window.mqttData && mqttData[t]; return v === undefined ? undefined : String(v); };
const zahl = t => { const v = Number(text(t)); return isFinite(v) ? v : null; };
function sekundenZuZeit(s) {
if (s === null) return null;
const h = Math.floor(s / 3600) % 24, m = Math.floor(s % 3600 / 60);
return String(h).padStart(2, "0") + ":" + String(m).padStart(2, "0");
}
/** "07:00" -> "heute 07:00" oder "morgen 07:00", je nachdem, was als nächstes kommt. */
function naechstes(zeit) {
const [h, m] = zeit.split(":").map(Number);
const jetzt = new Date(), ziel = new Date();
ziel.setHours(h, m, 0, 0);
return (ziel > jetzt ? "heute " : "morgen ") + zeit;
}
function inStunden(h) {
const d = new Date(Date.now() + h * 3600000);
return (d.getDate() !== new Date().getDate() ? "morgen " : "um ") + hhmm(d);
}
window.ladefensterStarten = function (wurzel, url) {
const box = WALLBOXEN[wurzel.dataset.wallbox];
if (!box) return;
const whProProzent = Number(wurzel.dataset.whProProzent);
const $ = name => wurzel.querySelector('[data-lade="' + name + '"]');
const lokal = {}; // name -> {wert, bis}
let ziehen = false;
const merke = (name, wert) => { lokal[name] = { wert, bis: Date.now() + HALTEN }; };
const gilt = (name, gemeldet) => (lokal[name] && lokal[name].bis > Date.now()) ? lokal[name].wert : gemeldet;
function meldung(textInhalt, art) {
const m = $("meldung");
m.textContent = textInhalt;
m.className = "lade-meldung" + (art ? " " + art : "");
clearTimeout(meldung.uhr);
if (art === "gut") meldung.uhr = setTimeout(() => { m.textContent = "Änderungen gelten sofort"; m.className = "lade-meldung"; }, 2200);
}
async function senden(daten) {
meldung("wird geschickt …");
try {
const antwort = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(daten) });
const d = await antwort.json().catch(() => ({}));
if (!antwort.ok || d.error) throw new Error(d.error || "HTTP " + antwort.status);
meldung("✓ an die Wallbox geschickt", "gut");
} catch (e) {
meldung("Nicht übernommen: " + e.message, "fehler");
}
zeichnen();
}
/* ------------------------------------------------ Stand aus MQTT */
function stand() {
const soc = box.soc ? zahl(box.soc) : null;
const energieWh = zahl(box.energie);
const zeit = gilt("zeit", sekundenZuZeit(zahl(box.zeit)) || "07:00");
// Ziel: mit Ladestand als Prozent des Akkus, ohne als zusätzliche Prozent.
const gemeldetZiel = energieWh === null ? 80
: soc !== null ? Math.min(100, Math.round((soc + energieWh / whProProzent) / 5) * 5)
: Math.min(100, Math.max(5, Math.round(energieWh / whProProzent / 5) * 5));
return {
soc,
modus: gilt("modus", box.modi[text(box.modus)] || null),
ampere: gilt("ampere", zahl(box.ampere)),
frc: gilt("frc", box.frcWerte[text(box.frc)] ?? 0),
auto: box.autoWerte[text(box.auto)] || null,
kw: zahl(box.leistungKW) || 0,
restMin: box.restMin ? zahl(box.restMin) : null,
ziel: gilt("ziel", gemeldetZiel),
zeit,
netz: zahl("solarManager/P_Grid") || 0,
};
}
/* ------------------------------------------------------ Zeichnen */
function zeichnen() {
const s = stand();
const laedt = s.kw >= 0.2 || s.auto === "laedt";
const mitSoc = s.soc !== null;
// Ring
const ist = mitSoc ? s.soc : 0;
const ziel = s.modus === "plan" ? s.ziel : mitSoc ? 100 : 0;
const zielBogen = mitSoc ? Math.max(0, ziel - ist) : (s.modus === "plan" ? s.ziel : 0);
const istBogen = wurzel.querySelector(".lade-ist");
istBogen.style.strokeDasharray = ist + " 100";
// Ein Bogen der Länge 0 zeigt mit runden Enden einen Punkt.
istBogen.style.opacity = ist > 0 ? "1" : "0";
const zb = wurzel.querySelector(".lade-ziel");
zb.style.strokeDasharray = zielBogen + " 100";
zb.style.strokeDashoffset = -ist;
setze("soc", mitSoc ? Math.round(s.soc) + " %" : (Number(wurzel.dataset.akkuKwh) ? komma(Number(wurzel.dataset.akkuKwh)) + " kWh" : ""));
setze("ringUnten", s.modus === "plan" ? (mitSoc ? "Ziel " + s.ziel + " %" : "+ " + s.ziel + " %") : mitSoc ? "Ladestand" : "Akku");
// Zustand
const zustand = laedt ? "lädt" : s.frc === 1 ? "gestoppt" : s.auto === "fertig" ? "fertig" : s.auto === "kein" ? "kein Auto" : "wartet";
$("leistung").innerHTML = komma(s.kw) + " kW<small>" + zustand + "</small>";
const pille = $("stecker");
const pilleText = s.auto === "kein" ? "kein Auto" : s.auto === "laedt" ? "lädt" : s.auto === "fertig" ? "voll" : s.auto ? "angesteckt" : "";
setze("stecker", pilleText);
pille.className = "lade-pille" + (s.auto && s.auto !== "kein" ? " an" : "");
const anteil = laedt && s.kw > 0 ? Math.max(0, Math.min(1, 1 - Math.max(0, s.netz) / (s.kw * 1000))) : null;
setze("quelle", anteil !== null ? Math.round(anteil * 100) + " % aus Sonnenstrom" : "");
let fertig = "";
if (laedt && s.restMin > 0) fertig = "fertig " + inStunden(s.restMin / 60);
else if (s.modus === "plan") fertig = "Ziel bis " + naechstes(s.zeit);
setze("fertig", fertig);
// Knopf
const knopf = $("start");
let aktion, beschriftung, klasse = "";
if (laedt) { aktion = 1; beschriftung = "Laden stoppen"; klasse = "laeuft"; }
else if (s.frc === 1) { aktion = 0; beschriftung = "Ladung freigeben"; klasse = "gesperrt"; }
else if (s.frc === 2) { aktion = 0; beschriftung = "Zurück zur Automatik"; }
else if (s.auto === "kein") { aktion = 2; beschriftung = "Kein Auto angesteckt"; klasse = "aus"; }
else { aktion = 2; beschriftung = "Jetzt laden"; }
knopf.dataset.aktion = aktion;
knopf.disabled = klasse === "aus";
knopf.className = "lade-start " + klasse;
setze("startText", beschriftung);
// Erklärung
const kwMax = KW_JE_A[s.ampere] || (s.ampere ? s.ampere * 0.69 : 11);
let erkl = {
sofort: "Lädt mit bis zu " + komma(kwMax) + " kW reicht die Sonne nicht, kommt der Rest aus dem Netz.",
sonne: "Lädt nur mit Überschuss. Scheint zu wenig Sonne, pausiert die Ladung.",
plan: "Lädt bis " + naechstes(s.zeit) + (mitSoc ? " auf " + s.ziel + " %" : " " + s.ziel + " % nach") + " bevorzugt mit Sonnenstrom, nötigenfalls aus dem Netz.",
}[s.modus] || "";
if (s.frc === 1) erkl = "Gestoppt: die Wallbox lädt erst wieder nach „Ladung freigeben“ dann entscheidet der Modus.";
if (s.frc === 2) erkl = "Lädt sofort, unabhängig vom Modus. „Zurück zur Automatik“ überlässt es wieder dem Modus.";
setze("erklaerung", erkl);
// Modus, Strom
wurzel.querySelectorAll("[data-modus]").forEach(b => b.setAttribute("aria-checked", b.dataset.modus === s.modus ? "true" : "false"));
wurzel.querySelectorAll("[data-ampere]").forEach(b => b.setAttribute("aria-checked", Number(b.dataset.ampere) === s.ampere ? "true" : "false"));
// Planung
const planung = $("planung");
planung.hidden = s.modus !== "plan";
if (!planung.hidden) {
const regler = $("ziel");
if (!ziehen) regler.value = s.ziel;
const wert = Number(regler.value);
regler.style.setProperty("--ist", (mitSoc ? s.soc : 0) + "%");
regler.style.setProperty("--wert", wert + "%");
setze("zielTitel", mitSoc ? "Ladestand bei Abfahrt" : "Zusätzlich laden");
setze("markeMitte", mitSoc ? "jetzt " + Math.round(s.soc) + " %" : "");
const plusWh = mitSoc ? Math.max(0, wert - s.soc) * whProProzent : wert * whProProzent;
$("zielAusgabe").innerHTML = wert + " %<small>" + (plusWh > 0 ? "+ " + komma(plusWh / 1000) + " kWh" : "schon erreicht") + "</small>";
setze("abfahrtAusgabe", naechstes(s.zeit));
let bekannt = false;
wurzel.querySelectorAll("[data-zeit]").forEach(c => {
const an = c.dataset.zeit === s.zeit;
bekannt = bekannt || an;
c.setAttribute("aria-pressed", an ? "true" : "false");
});
$("chipEigen").setAttribute("aria-pressed", bekannt ? "false" : "true");
const eigen = $("zeitEigen");
if (document.activeElement !== eigen && !bekannt) eigen.value = s.zeit;
}
return s;
}
function setze(name, inhalt) {
const e = $(name);
if (e && e.textContent !== inhalt) e.textContent = inhalt;
}
/* ------------------------------------------------------ Aktionen */
function planSchicken(s) {
const ziel = Number($("ziel").value);
let wh = s.soc !== null ? Math.max(0, ziel - s.soc) * whProProzent : ziel * whProProzent;
wh = Math.max(100, Math.round(wh));
merke("modus", "plan");
merke("ziel", ziel);
merke("zeit", s.zeit);
senden({ aktion: "plan", wh, zeit: s.zeit });
}
$("start").addEventListener("click", () => {
const wert = Number($("start").dataset.aktion);
merke("frc", wert);
senden({ aktion: "frc", wert });
});
wurzel.querySelectorAll("[data-modus]").forEach(b => b.addEventListener("click", () => {
const modus = b.dataset.modus;
if (modus === "plan") { merke("modus", "plan"); planSchicken(stand()); return; }
merke("modus", modus);
senden({ aktion: "modus", modus });
}));
wurzel.querySelectorAll("[data-ampere]").forEach(b => b.addEventListener("click", () => {
const ampere = Number(b.dataset.ampere);
merke("ampere", ampere);
senden({ aktion: "strom", ampere });
}));
$("ziel").addEventListener("input", () => { ziehen = true; merke("ziel", Number($("ziel").value)); zeichnen(); });
$("ziel").addEventListener("change", () => { ziehen = false; planSchicken(stand()); });
wurzel.querySelectorAll("[data-zeit]").forEach(c => c.addEventListener("click", () => {
merke("zeit", c.dataset.zeit);
planSchicken(stand());
}));
$("zeitEigen").addEventListener("change", e => {
if (!/^\d\d:\d\d$/.test(e.target.value)) return;
merke("zeit", e.target.value);
planSchicken(stand());
});
zeichnen();
const takt = setInterval(zeichnen, 1000);
const fenster = wurzel.closest(".modal");
if (fenster) fenster.addEventListener("hidden.bs.modal", () => clearInterval(takt), { once: true });
};
})();
+5 -60
View File
@@ -442,17 +442,10 @@ var chartSettings = {
// Setzt den Schieberegler aus den Wattstunden, die die Wallbox meldet.
function setzeLadeRegler(wattstunden) {
const regler = document.getElementById("modal-slider");
const whProProzent = Number(regler.dataset.whProProzent);
if (whProProzent > 0) {
regler.value = Math.round(Number(String(wattstunden)) / whProProzent);
}
}
function openModal(type) {
document.getElementById("modalSaveBtn").style.display = type === "watering" ? "none" : "";
// Das Ladefenster und die Bewaesserung uebernehmen jede Aenderung sofort -
// "Save changes" braucht nur noch der Heizstab.
document.getElementById("modalSaveBtn").style.display = type === "heater" ? "" : "none";
switch(type){
case "CarEG":
document.getElementById("modal-title").innerHTML = "Autoladung EG";
@@ -496,57 +489,9 @@ var chartSettings = {
var span = document.getElementById("modal-slider-label");
switch(type){
case "CarEG":
bindModalSlider(slider, span, value => value + "%");
document.getElementById("evStart/Stop").addEventListener("click", submitFormAjax);
document.getElementById("evStart/Stop").contentURL = contentURL;
mode = new TextDecoder().decode(mqttData["wattpilot/properties/lmo/state"]);
time = String(mqttData["wattpilot/properties/ftt/state"]);
energy = mqttData["wattpilot/properties/fte/state"];
amp = new TextDecoder().decode(mqttData["wattpilot/properties/amp/state"]);
car = new TextDecoder().decode(mqttData["wattpilot/properties/car/state"]);
charge = Number(mqttData["solarManager/p_l1ev"])+Number(mqttData["solarManager/p_l2ev"])+Number(mqttData["solarManager/p_l3ev"])
if (charge < 0.2) { document.getElementById("evStart/Stop").innerHTML = 'Laden starten' }
else { document.getElementById("evStart/Stop").innerHTML = 'Laden stoppen' }
if (mode == "Awattar") { document.getElementById("EV_Eco").click(); }
else if (mode == "Default") { document.getElementById("EV_Default").click(); }
else if (mode == "AutomaticStop") { document.getElementById("EV_NextTrip").click(); }
else { alert(mode); }
document.getElementById("EV_NextTripTime").value = time.toHHMM();
// Wattstunden je Prozent liefert das Formular mit (carForm.php);
// die Akkugroesse steht in restricted/config.php. Frueher standen
// hier feste 14000 Wh - fuer beide Autos dasselbe, und fuer keines
// von beiden richtig.
setzeLadeRegler(energy);
document.getElementById("modal-slider").dispatchEvent(new Event('input'));
if (amp == "16") { document.getElementById("EV_16A").click(); }
else if (amp == "10") { document.getElementById("EV_10A").click(); }
else if (amp == "6") { document.getElementById("EV_6A").click(); }
break;
case "CarOG":
bindModalSlider(slider, span, value => value + "%");
document.getElementById("evStart/Stop").addEventListener("click", submitFormAjax);
document.getElementById("evStart/Stop").contentURL = contentURL;
mode = mqttData["go-eCharger/270003/lmo"];
time = String(mqttData["go-eCharger/270003/att"]);
energy = mqttData["go-eCharger/270003/ate"];
amp = mqttData["go-eCharger/270003/amp"];
car = mqttData["go-eCharger/270003/car"];
if (car != "2") { document.getElementById("evStart/Stop").innerHTML = 'Laden starten' }
else { document.getElementById("evStart/Stop").innerHTML = 'Laden stoppen' }
if (mode == "4") { document.getElementById("EV_Eco").click(); }
else if (mode == "3") { document.getElementById("EV_Default").click(); }
else if (mode == "5") { document.getElementById("EV_NextTrip").click(); }
else { alert(mode); }
document.getElementById("EV_NextTripTime").value = time.toHHMM();
// Wattstunden je Prozent liefert das Formular mit (carForm.php);
// die Akkugroesse steht in restricted/config.php. Frueher standen
// hier feste 14000 Wh - fuer beide Autos dasselbe, und fuer keines
// von beiden richtig.
setzeLadeRegler(energy);
document.getElementById("modal-slider").dispatchEvent(new Event('input'));
if (amp == "16") { document.getElementById("EV_16A").click(); }
else if (amp == "10") { document.getElementById("EV_10A").click(); }
else if (amp == "6") { document.getElementById("EV_6A").click(); }
// Alles Weitere in js/solar/ladefenster.js.
ladefensterStarten(modalBodyElement.querySelector(".lade"), contentURL);
break;
case "heater":
bindModalSlider(slider, span, value => value == 0 ? "Automatik" : (value / 10) + " kW");