diff --git a/ajax/carEG.php b/ajax/carEG.php
index 6214812..de58a81 100644
--- a/ajax/carEG.php
+++ b/ajax/carEG.php
@@ -1,73 +1,12 @@
-&1", $output, $return_var);
- } elseif ($_POST["evStart/Stop"] == "Laden stoppen") {
- $close = 1;
- exec("python wattpilot.py -stop 2>&1", $output, $return_var);
- }
- $close = 1;
- } else if (isset($_POST["evMode"])) {
- // Eingaben pruefen, bevor sie in die Kommandozeile gehen. ftt kam bisher
- // ungeprueft aus dem Formular und liess sich mit "12:00; befehl" zum
- // Anhaengen beliebiger Shell-Befehle missbrauchen.
- $fte = intval($_POST["fte"]);
- if ($fte > 100)
- $fte = 100;
- else if ($fte < 1)
- $fte = 1;
- $fte = $fte * 140;
-
- $evAmp = intval($_POST["evAmp"]);
- if ($evAmp > 16)
- $evAmp = 16;
- if ($evAmp < 6)
- $evAmp = 6;
-
- // Das Formularfeld ist ein , liefert also HH:MM.
- // Alles andere gilt als "keine Ladeplanung" und faellt in den else-Zweig.
- $ftt = "";
- if (isset($_POST["ftt"]) && preg_match('/^([01][0-9]|2[0-3]):[0-5][0-9]$/', $_POST["ftt"])) {
- $ftt = $_POST["ftt"];
- }
- $setTime = gmdate("Y-m-d\TH:i:s", time()) . ".000";
-
- if ($_POST["evMode"] == "eco") {
- $close = 1;
- if ($ftt !== "" && $fte > 0) {
- exec("python wattpilot.py -eco -time " . escapeshellarg($ftt) . " -energy " . $fte . " -maxCurr " . $evAmp . " -setTime " . escapeshellarg($setTime) . " 2>&1", $output, $return_var);
- } else {
- exec("/bin/python wattpilot.py -e 2>&1", $output, $return_var);
- }
- } else if ($_POST["evMode"] == "default") {
- $close = 1;
- if ($ftt !== "" && $fte > 0) {
- exec("python wattpilot.py -default -time " . escapeshellarg($ftt) . " -energy " . $fte . " -maxCurr " . $evAmp . " -setTime " . escapeshellarg($setTime) . " 2>&1", $output, $return_var);
- } else {
- exec("python wattpilot.py -d 2>&1", $output, $return_var);
- }
- } else if ($_POST["evMode"] == "NextTrip") {
- $close = 1;
- if ($ftt !== "" && $fte > 0) {
- exec("python wattpilot.py -trip -time " . escapeshellarg($ftt) . " -energy " . $fte . " -maxCurr " . $evAmp . " -setTime " . escapeshellarg($setTime) . " 2>&1", $output, $return_var);
- } else {
- exec("python wattpilot.py -t 2>&1", $output, $return_var);
- }
- }
- $close = 1;
- }
-} else {
- $close = 1;
-}
-if (!$close) {
- $formId = "carEG_form";
- include __DIR__ . "/carForm.php";
-}
+ 100)
- $_POST["fte"] = 100;
- else if ($_POST["fte"] < 0)
- $_POST["fte"] = 0;
- if ($_POST["evAmp"] > 16)
- $_POST["evAmp"] = 16;
- if ($_POST["evAmp"] < 6)
- $_POST["evAmp"] = 6;
- $_POST["fte"] = $_POST["fte"] * 90;
- $seconds = strtotime('1970-01-01 ' . $_POST["ftt"] . ':00GMT');
- if ($_POST["evMode"] == "eco") {
- $close = 1;
- if ($_POST["ftt"] && $_POST["fte"] > 0) {
- httpGetJson("http://192.168.179.122/api/set?ate=" . $_POST["fte"] . "&att=" . $seconds . "&=" . $_POST["evAmp"] . "&fup=true&lmo=4");
- } else {
- httpGetJson("http://192.168.179.122/api/set?amp=" . $_POST["evAmp"] . "&fup=true&lmo=4");
- }
- } else if ($_POST["evMode"] == "default") {
- $close = 1;
- if ($_POST["ftt"] && $_POST["fte"] > 0) {
- httpGetJson("http://192.168.179.122/api/set?ate=" . $_POST["fte"] . "&att=" . $seconds . "&=" . $_POST["evAmp"] . "&fup=true&lmo=3");
- } else {
- httpGetJson("http://192.168.179.122/api/set?amp=" . $_POST["evAmp"] . "&fup=true&lmo=3");
- }
- } else if ($_POST["evMode"] == "NextTrip") {
- $close = 1;
- if ($_POST["ftt"] && $_POST["fte"] > 0) {
- httpGetJson("http://192.168.179.122/api/set?ate=" . $_POST["fte"] . "&att=" . $seconds . "&=" . $_POST["evAmp"] . "&fup=true&lmo=5");
- } else {
- httpGetJson("http://192.168.179.122/api/set?amp=" . $_POST["evAmp"] . "&fup=true&lmo=5");
- }
- }
- $close = 1;
- }
-} else {
- $close = 1;
-}
-if (!$close) {
- $formId = "carOG_form";
- include __DIR__ . "/carForm.php";
-}
+ 3, // Immer laden
+ "eco" => 4, // Ueberschuss (Awattar)
+ "NextTrip" => 5, // Ladeplanung (AutomaticStop)
+];
+
+$close = 0;
+
+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, deshalb steht er je Wallbox in der Tabelle.
+ $prozent = min(100, max(0, intval($_POST["fte"] ?? 0)));
+ $energie = $prozent * $konf["whProProzent"];
+
+ $strom = min(16, max(6, intval($_POST["evAmp"] ?? 6)));
+
+ // Das Formularfeld ist ein , 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;
+ }
+
+ // Reihenfolge zaehlt: Ziel zuerst, Modus zuletzt.
+ $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["fup"] = true;
+ $werte["lmo"] = $modus;
+
+ wallboxSetzen($wallbox, $werte);
+ }
+ $close = 1;
+ }
+} else {
+ $close = 1;
+}
+
+if (!$close) {
+ include __DIR__ . "/carForm.php";
+}
diff --git a/restricted/wallboxen.php b/restricted/wallboxen.php
new file mode 100644
index 0000000..ff8d911
--- /dev/null
+++ b/restricted/wallboxen.php
@@ -0,0 +1,128 @@
+/set` entgegen und antworten auf
+ * `/result` mit `success` oder einer Fehlermeldung.
+ *
+ * Der Lesestand liegt ohnehin schon auf demselben Broker - der Browser
+ * abonniert `go-eCharger/270003/...` und `wattpilot/properties/.../state`
+ * nebeneinander (js/solar/solarMQTT.js).
+ */
+
+require_once(__DIR__ . "/../ajax/phpMQTT.php");
+
+// Der Broker laeuft auf demselben Rechner wie der Webserver. Steht hier und
+// nicht in commands.php, damit diese Datei nicht die halbe Raumsteuerung
+// mitzieht, nur um zwei Konstanten zu erben.
+const WALLBOX_BROKER = "localhost";
+const WALLBOX_PORT = 1883;
+
+/**
+ * Die beiden Wallboxen und worin sie sich noch unterscheiden.
+ *
+ * Die Schluessel `frc`, `lmo`, `amp` und `fup` heissen auf beiden gleich -
+ * es ist beides go-e-Firmware. Uebrig bleiben drei echte Unterschiede, und
+ * die stehen hier statt in zwei getrennten Codepfaden.
+ */
+function wallboxen()
+{
+ return [
+ "eg" => [
+ "titel" => "Wattpilot (EG)",
+ "praefix" => "wattpilot/properties/",
+ // Die Bruecke haengt "/set" an den Eigenschaftsnamen an.
+ "namen" => [],
+ // Wattstunden je Prozent des Schiebereglers - Akku des Autos,
+ // das hier haengt.
+ "whProProzent" => 140,
+ // Zeichenketten gehen roh hinaus; die Bruecke gibt sie
+ // unveraendert an die Wallbox weiter.
+ "textInJson" => false,
+ // Die Ladeplanung rechnet mit der Uhr der Wallbox. Beim alten
+ // Python-Aufruf wurde sie vor jedem Moduswechsel gestellt
+ // (-setTime); das bleibt so, damit die Funktion nicht verloren
+ // geht. Der go-e hat das nie gebraucht.
+ "uhrStellen" => true,
+ ],
+ "og" => [
+ "titel" => "go-e (OG)",
+ "praefix" => "go-eCharger/270003/",
+ // Zielzeit und Zielenergie heissen hier anders. Die Wattpilot
+ // kennt "ate"/"att" gar nicht, der go-e umgekehrt "fte"/"ftt".
+ "namen" => ["fte" => "ate", "ftt" => "att"],
+ "whProProzent" => 90,
+ // Der go-e liest jeden Wert als JSON. Zeichenketten muessen
+ // deshalb in Anfuehrungszeichen stehen, sonst antwortet er mit
+ // "tried to set api key with invalid json".
+ "textInJson" => true,
+ "uhrStellen" => false,
+ ],
+ ];
+}
+
+/**
+ * Einen Wert so schreiben, wie die jeweilige Wallbox ihn erwartet.
+ */
+function wallboxNutzlast($wert, array $konf)
+{
+ if (is_bool($wert)) {
+ return $wert ? "true" : "false";
+ }
+ if (is_string($wert) && $konf["textInJson"]) {
+ return json_encode($wert);
+ }
+ return (string) $wert;
+}
+
+/**
+ * Mehrere Eigenschaften einer Wallbox setzen.
+ *
+ * Die Reihenfolge von $werte bleibt erhalten - sie ist nicht beliebig: der
+ * Modus (`lmo`) kommt zuletzt, damit Zielzeit und Zielenergie schon stehen,
+ * wenn er greift.
+ *
+ * Liefert true, wenn alles beim Broker war. Ob die Wallbox die Werte
+ * genommen hat, steht anschliessend auf den `/result`-Topics; darauf wird
+ * hier nicht gewartet, damit ein Klick nicht an einer stummen Wallbox
+ * haengenbleibt.
+ */
+function wallboxSetzen(string $box, array $werte): bool
+{
+ $alle = wallboxen();
+ if (!isset($alle[$box])) {
+ throw new InvalidArgumentException("Unbekannte Wallbox: " . $box);
+ }
+ $konf = $alle[$box];
+
+ $mqtt = new Bluerhinos\phpMQTT(WALLBOX_BROKER, WALLBOX_PORT,
+ "WallboxWeb-" . bin2hex(random_bytes(4)));
+ if (!$mqtt->connect(true, null, "", "")) {
+ error_log("wallboxSetzen: kein Kontakt zum MQTT-Broker");
+ return false;
+ }
+
+ foreach ($werte as $schluessel => $wert) {
+ $name = $konf["namen"][$schluessel] ?? $schluessel;
+ $mqtt->publish($konf["praefix"] . $name . "/set",
+ wallboxNutzlast($wert, $konf), 0, false);
+ }
+
+ $mqtt->close();
+ return true;
+}