From 61ea98db1d5b8da23369d522f87e4fa90a6cb73f Mon Sep 17 00:00:00 2001 From: Moirtz Wagner Date: Fri, 4 Sep 2026 08:36:13 +0200 Subject: [PATCH] Beide Wallboxen werden gleich bedient carEG.php und carOG.php waren fast identisch und unterschieden sich nur im Transportweg: der go-e bekam seine Kommandos per HTTP, die Wattpilot ueber einen eigenen Python-Prozess je Klick, der sich einen Websocket aufbaute, anmeldete, ein Feld setzte und wieder starb. Beide haengen laengst am selben MQTT-Broker - der go-e mit eigenem Client (mce=true, mcr=false), die Wattpilot ueber wattpilot_bruecke.py. Beide nehmen Kommandos auf /set entgegen. Die Steuerung steht deshalb jetzt einmal in carSteuerung.php; was die Wallboxen wirklich trennt - Topic-Praefix, die Namen ate/att gegen ftt/fte und die Wattstunden je Prozent - steht als Tabelle in restricted/wallboxen.php. Nebenbei faellt damit /volume1/homes/wagner/wattpilot.py weg, das das Passwort der Wallbox im Klartext enthaelt und ausserhalb jedes Repos liegt. Co-Authored-By: Claude Opus 5 --- ajax/carEG.php | 85 ++++---------------------- ajax/carOG.php | 70 ++++----------------- ajax/carSteuerung.php | 87 ++++++++++++++++++++++++++ restricted/wallboxen.php | 128 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 239 insertions(+), 131 deletions(-) create mode 100644 ajax/carSteuerung.php create mode 100644 restricted/wallboxen.php 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; +}