diff --git a/data/config/apache/000-default.conf b/data/config/apache/000-default.conf
index 4a16b41bec..f84a368a78 100644
--- a/data/config/apache/000-default.conf
+++ b/data/config/apache/000-default.conf
@@ -1,4 +1,4 @@
-# openwb-version:6
+# openwb-version:7
# The ServerName directive sets the request scheme, hostname and port that
# the server uses to identify itself. This is used when creating
@@ -40,6 +40,11 @@
# Proxy WebSocket and MQTT connections to Mosquitto
# ToDo: remove /mqtt target once all clients use /ws
ProxyPassMatch "^/(ws|mqtt)(/|$)" "ws://127.0.0.1:9003/"
+
+ # Proxy simpleAPI requests to the Python HTTP server in simpleAPI_mqtt.py
+ # (port from data/config/simpleapi_config.json's "http" section).
+ ProxyPassMatch "^/openWB/simpleAPI(/|$)" "http://127.0.0.1:8099/"
+ ProxyPassReverse "/openWB/simpleAPI/" "http://127.0.0.1:8099/"
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
diff --git a/data/config/apache/apache-openwb-ssl.conf b/data/config/apache/apache-openwb-ssl.conf
index ddd95d2a1f..c3b3930b4d 100644
--- a/data/config/apache/apache-openwb-ssl.conf
+++ b/data/config/apache/apache-openwb-ssl.conf
@@ -1,4 +1,4 @@
-# openwb-version:8
+# openwb-version:9
ServerAdmin webmaster@localhost
@@ -143,6 +143,11 @@
# ToDo: remove /mqtt target once all clients use /ws
ProxyPassMatch "^/(ws|mqtt)(/|$)" "ws://127.0.0.1:9003/"
+ # Proxy simpleAPI requests to the Python HTTP server in simpleAPI_mqtt.py
+ # (port from data/config/simpleapi_config.json's "http" section).
+ ProxyPassMatch "^/openWB/simpleAPI(/|$)" "http://127.0.0.1:8099/"
+ ProxyPassReverse "/openWB/simpleAPI/" "http://127.0.0.1:8099/"
+
diff --git a/data/config/apache/localhost.conf b/data/config/apache/localhost.conf
index 4d809119de..4d06ab119d 100644
--- a/data/config/apache/localhost.conf
+++ b/data/config/apache/localhost.conf
@@ -1,4 +1,4 @@
-# openwb-version:2
+# openwb-version:3
Listen 127.0.0.1:81
@@ -42,6 +42,11 @@ Listen 127.0.0.1:81
# Proxy WebSocket and MQTT connections to Mosquitto
# ToDo: remove /mqtt target once all clients use /ws
ProxyPassMatch "^/(ws|mqtt)(/|$)" "ws://127.0.0.1:9003/"
+
+ # Proxy simpleAPI requests to the Python HTTP server in simpleAPI_mqtt.py
+ # (port from data/config/simpleapi_config.json's "http" section).
+ ProxyPassMatch "^/openWB/simpleAPI(/|$)" "http://127.0.0.1:8099/"
+ ProxyPassReverse "/openWB/simpleAPI/" "http://127.0.0.1:8099/"
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
diff --git a/data/config/openwb-simpleAPI.service b/data/config/openwb-simpleAPI.service
index b549c9223b..6f9c751249 100644
--- a/data/config/openwb-simpleAPI.service
+++ b/data/config/openwb-simpleAPI.service
@@ -6,7 +6,7 @@ After=mosquitto.service
[Service]
User=openwb
WorkingDirectory=/var/www/html/openWB
-ExecStart=/var/www/html/openWB/simpleAPI/simpleAPI_mqtt.py
+ExecStart=/var/www/html/openWB/simpleAPI/simpleapi.py
Restart=always
# extend timeout to 15min for long running atreboot
TimeoutStartSec=900
diff --git a/data/config/simpleAPI_mqtt_config.json b/data/config/simpleAPI_mqtt_config.json
deleted file mode 100644
index 5010dc36f7..0000000000
--- a/data/config/simpleAPI_mqtt_config.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "host": "localhost",
- "port": 1884,
- "username": null,
- "password": null,
- "use_tls": false,
- "validate_cert": false,
- "qos": 0,
- "retain": true,
- "reconnect_delay": 10,
- "log_level": "INFO"
-}
diff --git a/data/config/simpleapi_config.json b/data/config/simpleapi_config.json
new file mode 100644
index 0000000000..b694d8be03
--- /dev/null
+++ b/data/config/simpleapi_config.json
@@ -0,0 +1,26 @@
+{
+ "host": "localhost",
+ "port": 1884,
+ "username": null,
+ "password": null,
+ "use_tls": false,
+ "validate_cert": false,
+ "qos": 0,
+ "retain": true,
+ "reconnect_delay": 10,
+ "log_level": "INFO",
+ "http": {
+ "host": "127.0.0.1",
+ "port": 8099
+ },
+ "api": {
+ "cors_enabled": true
+ },
+ "auth": {
+ "enabled": false,
+ "require_https": false,
+ "tokens": [],
+ "users": {}
+ },
+ "debug": false
+}
diff --git a/runs/setup_apache2.sh b/runs/setup_apache2.sh
index d2f84bd4b6..a3560b7acb 100755
--- a/runs/setup_apache2.sh
+++ b/runs/setup_apache2.sh
@@ -74,6 +74,10 @@ echo "checking required apache modules..."
enableModule headers
enableModule ssl
enableModule proxy_wstunnel
+# needed for the simpleAPI ProxyPass(Match) to the local Python HTTP server
+# (previously only enabled for the Pro+ branch below, which proxies to a
+# remote Pro+ device over the same module)
+enableModule proxy_http
# default site (http and https)
echo "apache default site..."
@@ -138,12 +142,10 @@ if lsusb | grep -Eq 'RTL8153|AX88179'; then
echo "second network for pro plus detected"
# enable pro+ specific configurations
enableSite apache-proplus
- enableModule proxy_http
else
echo "no second network for pro plus detected"
# disable all pro+ specific configurations
disableSite apache-proplus
- disableModule proxy_http
fi
# restart apache if required
diff --git a/simpleAPI/config/config.php b/simpleAPI/config/config.php
deleted file mode 100644
index f6a537950d..0000000000
--- a/simpleAPI/config/config.php
+++ /dev/null
@@ -1,39 +0,0 @@
- [
- 'server' => 'localhost',
- 'port' => 8883,
- 'validate_cert' => false,
- 'username' => '',
- 'password' => '',
- 'clientid' => 'SimpleAPI_' . uniqid()
- ],
-
- // API Konfiguration
- 'api' => [
- 'cors_enabled' => true,
- 'max_request_size' => '10M'
- ],
-
- // Authentifizierung
- 'auth' => [
- 'enabled' => true,
- 'require_https' => false,
-
- // Gültige Tokens
- 'tokens' => [
- // 'your-secret-token-here'
- ],
-
- // Benutzer (Username => Passwort/Hash)
- 'users' => [
- // 'admin' => password_hash('admin123', PASSWORD_DEFAULT),
- // 'user' => 'plaintext_password'
- ]
- ],
-
- // Debug-Modus
- 'debug' => false
-];
diff --git a/simpleAPI/simpleapi.php b/simpleAPI/simpleapi.php
deleted file mode 100644
index 5c97ffa66b..0000000000
--- a/simpleAPI/simpleapi.php
+++ /dev/null
@@ -1,630 +0,0 @@
-config = require __DIR__ . '/config/config.php';
-
- // MQTT Client initialisieren
- $this->mqttClient = new MqttClient($this->config);
-
- // Parameter Handler initialisieren
- $this->parameterHandler = new ParameterHandler($this->mqttClient);
-
- // Authenticator initialisieren (mit MqttClient für Anmeldedaten-Test)
- $this->authenticator = new Authenticator($this->config, $this->mqttClient);
- }
-
- /**
- * HTTP Request verarbeiten
- */
- public function handleRequest()
- {
- try {
- // Content-Type setzen
- header('Content-Type: application/json');
-
- // CORS Headers wenn konfiguriert
- if ($this->config['api']['cors_enabled'] ?? false) {
- header('Access-Control-Allow-Origin: *');
- header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
- header('Access-Control-Allow-Headers: Content-Type, Authorization');
- }
-
- // OPTIONS Request für CORS
- if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
- http_response_code(200);
- return;
- }
-
- // Parameter sammeln (GET und POST)
- $params = array_merge($_GET, $_POST);
-
- // Debug-Modus
- $debugInfo = [];
- if (isset($params['debug']) && $params['debug'] === 'true') {
- $this->config['debug'] = true;
- }
-
- // Authentifizierung prüfen wenn erforderlich
- if (!$this->authenticator->authenticate($params)) {
- http_response_code(401);
- echo json_encode([
- 'success' => false,
- 'message' => 'Authentication failed'
- ]);
- return;
- }
-
- // MQTT-Anmeldedaten aus Parametern übernehmen falls vorhanden
- if (isset($params['username']) && isset($params['password'])) {
- $this->mqttClient->setCredentials($params['username'], $params['password']);
-
- if ($this->config['debug']) {
- $debugInfo[] = "Using MQTT credentials from parameters: username={$params['username']}";
- }
- } else {
- // Keine Parameter-Anmeldedaten vorhanden
- if ($this->config['debug']) {
- $debugInfo[] = "No credentials provided via parameters, using config or anonymous access";
- }
- }
-
- // Komponenten-Übersicht: alle verfügbaren IDs je Typ auflisten
- if (isset($params['list_components'])) {
- echo json_encode($this->mqttClient->findAllAvailableIds());
- return;
- }
-
- // Schreibvorgänge prüfen
- $writeParams = $this->getWriteParameters($params);
- if (!empty($writeParams)) {
- $result = $this->handleWriteRequest($writeParams, $params);
-
- // Raw-Output für Schreibvorgänge
- if (isset($params['raw']) && $params['raw'] === 'true') {
- if ($result['success']) {
- echo "OK"; // Erfolgreiche Schreibvorgänge -> "OK"
- } else {
- echo "ERROR: " . ($result['message'] ?? 'Unknown error');
- }
- } else {
- echo json_encode($result);
- }
- return;
- }
-
- // Lesevorgänge verarbeiten
- $readParams = $this->getReadParameters($params);
- if (!empty($readParams)) {
- $result = $this->handleReadRequest($readParams, $params, $debugInfo);
-
- // Raw-Ausgabe Validierung
- if (isset($params['raw']) && $params['raw'] === 'true') {
- if (count($readParams) > 1) {
- // Fehler: Raw-Output nur bei einzelnen Parametern erlaubt
- http_response_code(400);
- echo json_encode([
- 'success' => false,
- 'message' => 'Raw output (raw=true) is only allowed for single parameter requests',
- 'error' => 'Multiple parameters detected: ' . implode(', ', array_keys($readParams))
- ]);
- return;
- }
-
- // Prüfe ob der Parameter für Raw-Output geeignet ist
- $paramName = array_keys($readParams)[0];
- if ($this->isComplexParameter($paramName)) {
- http_response_code(400);
- echo json_encode([
- 'success' => false,
- 'message' => 'Raw output (raw=true) is not supported for complex parameters',
- 'error' => "Parameter '{$paramName}' returns multiple values. Use specific single-value parameters instead.",
- 'suggestion' => 'Try parameters like get_counter_power, get_counter_voltage_p1, etc.'
- ]);
- return;
- }
-
- // Einzelner Parameter: Raw-Output verwenden
- echo $this->formatRawOutput($result, $paramName);
- } else {
- // Für get_lastlivevaluesjson: JSON direkt ausgeben auch ohne raw=true
- if (count($readParams) === 1 && array_keys($readParams)[0] === 'get_lastlivevaluesjson') {
- echo $this->formatRawOutput($result, 'get_lastlivevaluesjson');
- } else {
- echo json_encode($result);
- }
- }
- return;
- }
-
- // Keine gültigen Parameter
- http_response_code(400);
- echo json_encode([
- 'success' => false,
- 'message' => 'No valid parameters provided'
- ]);
- } catch (Exception $e) {
- http_response_code(500);
- echo json_encode([
- 'success' => false,
- 'message' => 'Internal server error',
- 'error' => $this->config['debug'] ? $e->getMessage() : 'An error occurred'
- ]);
- }
- }
-
- /**
- * Schreibbare Parameter aus Request extrahieren
- */
- private function getWriteParameters($params)
- {
- $writeParams = [];
- $writeableKeys = [
- 'set_chargemode',
- 'chargecurrent',
- 'minimal_pv_soc',
- 'minimal_permanent_current',
- 'max_price_eco',
- 'chargepoint_lock',
- 'bat_mode',
- 'bat_power_reserve',
- 'instant_charging_limit',
- 'instant_charging_amount',
- 'instant_charging_soc',
- 'pv_charging_limit',
- 'pv_charging_amount',
- 'pv_charging_soc',
- 'vehicle',
- 'manual_soc',
- 'set_io_output'
- ];
-
- foreach ($writeableKeys as $key) {
- if (isset($params[$key])) {
- $writeParams[$key] = $params[$key];
- }
- }
-
- return $writeParams;
- }
-
- /**
- * Lesbare Parameter aus Request extrahieren
- */
- private function getReadParameters($params)
- {
- $readParams = [];
- $readableKeys = [
- // Chargepoint - Alle Daten
- 'get_chargepoint_all',
- // Chargepoint - Spannungen
- 'get_chargepoint_voltage_p1',
- 'get_chargepoint_voltage_p2',
- 'get_chargepoint_voltage_p3',
- 'get_chargepoint_voltages',
- // Chargepoint - Ströme
- 'get_chargepoint_current_p1',
- 'get_chargepoint_current_p2',
- 'get_chargepoint_current_p3',
- 'get_chargepoint_currents',
- // Chargepoint - Leistungen
- 'get_chargepoint_power',
- 'get_chargepoint_powers',
- // Chargepoint - Status & Energie
- 'get_chargepoint_imported',
- 'get_chargepoint_exported',
- 'get_chargepoint_daily_imported',
- 'get_chargepoint_daily_exported',
- 'get_chargepoint_monthly_exported',
- 'get_chargepoint_yearly_exported',
- 'get_chargepoint_frequency',
- 'get_chargepoint_rfid',
- 'get_chargepoint_rfid_timestamp',
- 'get_chargepoint_evse_current',
- 'get_chargepoint_power_factors',
- 'get_chargepoint_power_factor_p1',
- 'get_chargepoint_power_factor_p2',
- 'get_chargepoint_power_factor_p3',
- 'get_chargepoint_config_name',
- 'get_chargepoint_connected_vehicle_name',
- 'get_chargepoint_charge_template_name',
- 'get_chargepoint_charge_template_min_current',
- 'get_chargepoint_instant_charging_current',
- 'get_chargepoint_pv_charging_min_current',
- 'get_chargepoint_soc',
- 'get_chargepoint_state_str',
- 'get_chargepoint_fault_str',
- 'get_chargepoint_fault_state',
- 'get_chargepoint_phases_in_use',
- 'get_chargepoint_plug_state',
- 'get_chargepoint_charge_state',
- 'get_chargepoint_chargemode',
- // Counter - Alle Daten
- 'get_counter',
- // Counter - Spannungen
- 'get_counter_voltage_p1',
- 'get_counter_voltage_p2',
- 'get_counter_voltage_p3',
- 'get_counter_voltages',
- // Counter - Ströme
- 'get_counter_current_p1',
- 'get_counter_current_p2',
- 'get_counter_current_p3',
- 'get_counter_currents',
- // Counter - Leistungen
- 'get_counter_power',
- 'get_counter_powers',
- 'get_counter_power_factors',
- // Counter - Energie & Status
- 'get_counter_imported',
- 'get_counter_exported',
- 'get_counter_daily_imported',
- 'get_counter_daily_exported',
- 'get_counter_frequency',
- 'get_counter_fault_str',
- 'get_counter_fault_state',
- // Battery - Alle Daten
- 'battery',
- 'get_battery',
- // Battery - Einzelwerte
- 'get_battery_power',
- 'get_battery_soc',
- 'get_battery_currents',
- 'get_battery_imported',
- 'get_battery_exported',
- 'get_battery_daily_imported',
- 'get_battery_daily_exported',
- 'get_battery_fault_str',
- 'get_battery_fault_state',
- 'get_battery_power_limit_controllable',
- // PV - Alle Daten
- 'pv',
- 'get_pv',
- // PV - Einzelwerte
- 'get_pv_power',
- 'get_pv_currents',
- 'get_pv_imported',
- 'get_pv_exported',
- 'get_pv_daily_exported',
- 'get_pv_monthly_exported',
- 'get_pv_yearly_exported',
- 'get_pv_fault_str',
- 'get_pv_fault_state',
- // Consumer - Alle Daten
- 'consumer',
- 'get_consumer',
- 'get_consumer_all',
- // Consumer - Einzelwerte
- 'get_consumer_power',
- 'get_consumer_currents',
- 'get_consumer_voltages',
- 'get_consumer_powers',
- 'get_consumer_imported',
- 'get_consumer_exported',
- 'get_consumer_daily_imported',
- 'get_consumer_phases_in_use',
- 'get_consumer_charge_state',
- 'get_consumer_state_str',
- 'get_consumer_fault_str',
- 'get_consumer_fault_state',
- 'get_consumer_usage_type',
- // IO - Ausgaenge
- 'get_io_output_all',
- 'get_io_output',
- // System - Werte
- 'get_lastlivevaluesjson'
- ];
-
- foreach ($readableKeys as $key) {
- if (isset($params[$key])) {
- $readParams[$key] = $params[$key];
- }
- }
-
- return $readParams;
- }
-
- /**
- * Schreibanfrage verarbeiten
- */
- private function handleWriteRequest($writeParams, $allParams)
- {
- $firstTargetId = null;
-
- foreach ($writeParams as $param => $value) {
- $targetId = null;
- $idType = null;
-
- if ($this->isChargepointParameter($param)) {
- $targetId = $allParams['chargepoint_nr'] ?? null;
- $idType = 'chargepoint';
- } elseif ($this->isIoParameter($param)) {
- $targetId = $allParams['io_nr'] ?? null;
- $idType = 'io';
- }
-
- if ($idType !== null && ($targetId === null || $targetId === '' || $targetId === 'auto')) {
- try {
- $targetId = $this->mqttClient->getLowestId($idType);
- if ($targetId === null) {
- return [
- 'success' => false,
- 'message' => "No {$idType} devices available for auto-ID"
- ];
- }
- } catch (Exception $e) {
- return [
- 'success' => false,
- 'message' => "Auto-ID failed for {$idType}: " . $e->getMessage()
- ];
- }
- }
-
- $options = [
- 'io_output' => $allParams['io_output'] ?? null,
- 'io_output_type' => $allParams['io_output_type'] ?? null
- ];
-
- $result = $this->parameterHandler->writeParameter($param, $value, $targetId, $options);
-
- if ($firstTargetId === null) {
- $firstTargetId = $targetId;
- }
-
- if (!$result['success']) {
- return $result;
- }
- }
-
- // Erfolgreiche Antwort für ersten Parameter (OpenWB Kompatibilität)
- $firstParam = array_keys($writeParams)[0];
- $firstValue = $writeParams[$firstParam];
-
- return [
- 'success' => true,
- 'message' => $this->getSuccessMessage($firstParam, $firstValue, $firstTargetId ?? null),
- 'data' => [
- 'chargepoint_nr' => $this->isChargepointParameter($firstParam) ? $firstTargetId : null,
- 'io_nr' => $this->isIoParameter($firstParam) ? $firstTargetId : null,
- $firstParam => $firstValue
- ]
- ];
- }
-
- /**
- * Leseanfrage verarbeiten
- */
- private function handleReadRequest($readParams, $allParams, $debugInfo = [])
- {
- $result = [];
-
- // Debug-Informationen hinzufügen wenn vorhanden
- if (!empty($debugInfo) && $this->config['debug']) {
- $result['debug_info'] = $debugInfo;
- }
-
- foreach ($readParams as $param => $id) {
- try {
- // Auto-ID Feature: Niedrigste ID finden wenn "auto" oder leer
- if ($id === 'auto' || $id === '') {
- $type = $this->getTypeFromParameter($param);
- $id = $this->mqttClient->getLowestId($type);
-
- if ($id === null) {
- continue; // Skip wenn keine ID gefunden
- }
- }
-
- $options = [
- 'io_output' => $allParams['io_output'] ?? null,
- 'io_output_type' => $allParams['io_output_type'] ?? null
- ];
-
- $data = $this->parameterHandler->readParameter($param, $id, $options);
- if ($data !== null) {
- $result = array_merge($result, $data);
- }
- } catch (Exception $e) {
- // Wenn Auto-ID fehlschlägt, versuche ID 0 als Fallback
- if (($id === 'auto' || $id === '') && $this->config['debug']) {
- $result['debug_info'][] = "Auto-ID failed for $param: " . $e->getMessage();
- }
-
- // Fallback auf ID 0 für Chargepoints
- if (strpos($param, 'chargepoint') !== false) {
- try {
- $data = $this->parameterHandler->readParameter($param, 0);
- if ($data !== null) {
- $result = array_merge($result, $data);
- }
- } catch (Exception $fallbackError) {
- if ($this->config['debug']) {
- $result['debug_info'][] = "Fallback to ID 0 failed for $param: " . $fallbackError->getMessage();
- }
- }
- }
- }
- }
-
- return $result;
- }
-
- /**
- * Typ aus Parameter ermitteln
- */
- private function getTypeFromParameter($param)
- {
- if (strpos($param, 'chargepoint') !== false || strpos($param, 'get_chargepoint') !== false) {
- return 'chargepoint';
- } elseif (strpos($param, 'io') !== false) {
- return 'io';
- } elseif (strpos($param, 'battery') !== false) {
- return 'bat'; // MQTT Topic verwendet 'bat' nicht 'battery'
- } elseif (strpos($param, 'pv') !== false) {
- return 'pv';
- } elseif (strpos($param, 'counter') !== false) {
- return 'counter';
- } elseif (strpos($param, 'consumer') !== false) {
- return 'consumer';
- }
-
- return 'chargepoint'; // Fallback
- }
-
- /**
- * Prüfen ob Parameter komplex ist (mehrere Werte zurückgibt)
- */
- private function isComplexParameter($param)
- {
- $complexParameters = [
- 'get_chargepoint_all',
- 'get_counter',
- 'battery',
- 'get_battery',
- 'pv',
- 'get_pv',
- 'get_chargepoint_voltages',
- 'get_chargepoint_currents',
- 'get_chargepoint_powers',
- 'get_chargepoint_power_factors',
- 'get_counter_voltages',
- 'get_counter_currents',
- 'get_counter_powers',
- 'get_counter_power_factors',
- 'get_battery_currents',
- 'get_pv_currents',
- 'get_io_output_all',
- 'get_consumer_all',
- 'get_consumer',
- 'consumer',
- 'get_consumer_currents',
- 'get_consumer_voltages',
- 'get_consumer_powers'
- ];
-
- return in_array($param, $complexParameters);
- }
-
- /**
- * Prüfen ob Parameter zu Chargepoint gehört
- */
- private function isChargepointParameter($param)
- {
- $chargepointParameters = [
- 'set_chargemode',
- 'chargecurrent',
- 'minimal_pv_soc',
- 'minimal_permanent_current',
- 'max_price_eco',
- 'chargepoint_lock',
- 'bat_mode',
- 'bat_power_reserve',
- 'instant_charging_limit',
- 'instant_charging_amount',
- 'instant_charging_soc',
- 'pv_charging_limit',
- 'pv_charging_amount',
- 'pv_charging_soc',
- 'vehicle',
- 'manual_soc'
- ];
-
- return in_array($param, $chargepointParameters) || strpos($param, 'chargepoint') !== false;
- }
-
- /**
- * Pruefen ob Parameter zu IO gehoert
- */
- private function isIoParameter($param)
- {
- return in_array($param, ['set_io_output']) || strpos($param, 'get_io_') === 0;
- }
-
- /**
- * Erfolgs-Nachricht generieren
- */
- private function getSuccessMessage($param, $value, $chargepointId)
- {
- switch ($param) {
- case 'set_chargemode':
- return "Chargemode for chargepoint {$chargepointId} set to {$value}.";
- case 'chargecurrent':
- return "Chargecurrent for chargepoint {$chargepointId} set to {$value}A";
- case 'instant_charging_limit':
- return "Instant charging limit for chargepoint {$chargepointId} set to {$value}.";
- case 'instant_charging_amount':
- return "Instant charging amount for chargepoint {$chargepointId} set to {$value}kWh.";
- case 'instant_charging_soc':
- return "Instant charging SoC for chargepoint {$chargepointId} set to {$value}%.";
- case 'pv_charging_limit':
- return "PV charging limit for chargepoint {$chargepointId} set to {$value}.";
- case 'pv_charging_amount':
- return "PV charging amount for chargepoint {$chargepointId} set to {$value}kWh.";
- case 'pv_charging_soc':
- return "PV charging SoC for chargepoint {$chargepointId} set to {$value}%.";
- case 'vehicle':
- return "Vehicle {$value} assigned to chargepoint {$chargepointId}.";
- case 'manual_soc':
- return "Manual SoC set to {$value}% for chargepoint {$chargepointId}.";
- case 'set_io_output':
- return "IO output set to {$value} for io {$chargepointId}.";
- default:
- return "Parameter {$param} set to {$value}.";
- }
- }
-
- /**
- * Raw-Ausgabe formatieren
- */
- private function formatRawOutput($data, $paramName = null)
- {
- if (is_array($data)) {
- $firstKey = array_keys($data)[0];
- $firstValue = $data[$firstKey];
-
- // Für get_lastlivevaluesjson: JSON direkt zurückgeben
- if ($paramName === 'get_lastlivevaluesjson' && is_string($firstValue)) {
- return $firstValue;
- }
-
- // Für JSON-Strings: direkt den String-Wert zurückgeben
- if (is_string($firstValue)) {
- return $firstValue;
- }
-
- if (is_array($firstValue) && count($firstValue) === 1) {
- return array_values($firstValue)[0];
- }
-
- // Für andere Array-Strukturen: ersten Wert zurückgeben
- return $firstValue;
- }
-
- return $data;
- }
-}
-
-// API instanziieren und Request verarbeiten
-$api = new SimpleAPI();
-$api->handleRequest();
diff --git a/simpleAPI/simpleAPI_mqtt.py b/simpleAPI/simpleapi.py
similarity index 55%
rename from simpleAPI/simpleAPI_mqtt.py
rename to simpleAPI/simpleapi.py
index 821b3a3b60..b981e5265c 100755
--- a/simpleAPI/simpleAPI_mqtt.py
+++ b/simpleAPI/simpleapi.py
@@ -14,11 +14,14 @@
import sys
import ssl
import math
-from typing import Dict, Any, Optional
+from typing import Callable, Dict, Any, Optional, Set, Tuple
from pathlib import Path
import paho.mqtt.client as mqtt
import re
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+from src.http_server import build_http_server # noqa: E402
+
FORMAT_STR_SHORT = '%(asctime)s - %(message)s'
RAMDISK_PATH = str(Path(__file__).resolve().parents[1]) + '/ramdisk/'
@@ -28,7 +31,7 @@
file_handler.setFormatter(logging.Formatter(FORMAT_STR_SHORT))
log.addHandler(file_handler)
-CONFIG_FILE_PATH = "/var/www/html/openWB/data/config/simpleAPI_mqtt_config.json"
+CONFIG_FILE_PATH = "/var/www/html/openWB/data/config/simpleapi_config.json"
class SimpleMQTTDaemon:
@@ -44,9 +47,19 @@ def __init__(self, config_file: str):
# Cache for tracking lowest IDs per component type
self.lowest_ids: Dict[str, int] = {}
+ # Cache of every known ID per component type (used for list_components
+ # and as the basis for lowest_ids)
+ self.all_ids: Dict[str, Set[int]] = {}
+
# Cache for storing current charge_template configurations
self.charge_template_cache: Dict[str, Dict[str, Any]] = {}
+ # Raw per-topic cache (parsed payloads) backing the simpleAPI HTTP read
+ # parameters - distinct from value_cache, which holds the already
+ # transformed/simplified simpleAPI output topic tree, not the
+ # original openWB topics the HTTP read handlers need to mirror.
+ self.raw_value_cache: Dict[str, Any] = {}
+
# Track per-ID component values for efficient total topic aggregation
self.total_metrics = {
'pv': {'exported', 'monthly_exported', 'yearly_exported', 'imported', 'power'},
@@ -67,6 +80,8 @@ def __init__(self, config_file: str):
self.client.on_message = self._on_message
self.client.on_disconnect = self._on_disconnect
+ self.http_server = build_http_server(self, self.config)
+
def _load_config_file(self, config_file: str):
"""Load configuration from JSON file."""
try:
@@ -95,6 +110,8 @@ def _load_config_file(self, config_file: str):
numeric_level = getattr(logging, log_level.upper(), logging.INFO)
log.setLevel(numeric_level)
+ self.config = config
+
log.info(f"Loaded configuration from {config_file}")
except json.JSONDecodeError as e:
log.error(f"Invalid JSON in config file {config_file}: {e}")
@@ -117,12 +134,13 @@ def _on_connect(self, client, userdata, flags, rc):
client.subscribe("openWB/chargepoint/#", qos=0)
client.subscribe("openWB/counter/#", qos=0)
client.subscribe("openWB/consumer/#", qos=0)
+ client.subscribe("openWB/io/#", qos=0)
# Subscribe to simpleAPI set topics for write operations
client.subscribe("openWB/simpleAPI/set/#", qos=0)
log.info(
- "Subscribed to openWB component topics (bat, pv, chargepoint, counter, consumer) "
+ "Subscribed to openWB component topics (bat, pv, chargepoint, counter, consumer, io) "
"and simpleAPI set topics")
else:
log.error(f"Failed to connect to MQTT broker. Return code: {rc}")
@@ -181,32 +199,24 @@ def _on_message(self, client, userdata, msg):
# Parse payload once so we can reuse it for both total aggregation and normal transformation
parsed_payload = self._parse_payload(payload)
+ # Raw per-topic cache backing the simpleAPI HTTP read parameters
+ self.raw_value_cache[topic] = parsed_payload
+
# Update aggregated total topics from component ID metrics
self._update_total_topics(topic, parsed_payload)
# Transform and publish the message
- self._transform_and_publish(topic, payload, parsed_payload)
+ self._transform_and_publish(topic, parsed_payload)
except Exception as e:
log.error(f"Error processing message {msg.topic}: {e}")
- def _transform_and_publish(self, original_topic: str, payload: str, parsed_payload: Any = None):
+ def _transform_and_publish(self, original_topic: str, parsed_payload: Any):
"""Transform original topic to simpleAPI format and publish if value changed."""
try:
- log.debug(f"DEBUG: Processing original topic: {original_topic}")
-
- # Parse the payload
- if parsed_payload is None:
- parsed_payload = self._parse_payload(payload)
-
# Generate transformed topics
transformed_topics = self._generate_simple_topics(original_topic, parsed_payload)
- log.debug(
- f"DEBUG: Generated {len(transformed_topics)} transformed topics: "
- f"{list(transformed_topics.keys())}"
- )
-
# Publish each transformed topic if value changed
for topic, value in transformed_topics.items():
self._publish_if_changed(topic, value)
@@ -319,8 +329,10 @@ def _generate_simple_topics(self, original_topic: str, parsed_value: Any) -> Dic
if simple_base is None: # Topic should be filtered out
return result
- # Handle different value types
- self._expand_value_to_topics(simple_base, parsed_value, result)
+ # Handle different value types - simple_base was already run through the
+ # chargepoint transform above, so the top-level (non-recursive) expansion
+ # doesn't need to redo it.
+ self._expand_value_to_topics(simple_base, parsed_value, result, already_transformed=True)
# Generate simplified topics for lowest IDs
simplified_topics = self._generate_simplified_topics(simple_base, parsed_value)
@@ -331,25 +343,17 @@ def _generate_simple_topics(self, original_topic: str, parsed_value: Any) -> Dic
def _transform_chargepoint_topic(self, simple_base: str) -> Optional[str]:
"""Transform chargepoint topics according to simplified structure."""
- log.debug(f"DEBUG: _transform_chargepoint_topic input: {simple_base}")
+ log.debug(f"_transform_chargepoint_topic input: {simple_base}")
# FIRST: Handle connected_vehicle transformations (both /get/ and direct)
# This must happen BEFORE any other filtering
if '/connected_vehicle/config/chargemode' in simple_base:
- log.debug(f"DEBUG: Found chargemode pattern, transforming: {simple_base}")
- original = simple_base
simple_base = re.sub(r'/get/connected_vehicle/config/chargemode$', '/chargemode', simple_base)
- log.debug(f"DEBUG: After first regex: {simple_base}")
simple_base = re.sub(r'/connected_vehicle/config/chargemode$', '/chargemode', simple_base)
- log.debug(f"DEBUG: After second regex: {simple_base}")
- if original != simple_base:
- log.debug(f"DEBUG: Charge mode transformation successful: {original} -> {simple_base}")
elif '/connected_vehicle/info/name' in simple_base:
- log.debug(f"DEBUG: Found vehicle name pattern, transforming: {simple_base}")
simple_base = re.sub(r'/get/connected_vehicle/info/name$', '/vehicle_name', simple_base)
simple_base = re.sub(r'/connected_vehicle/info/name$', '/vehicle_name', simple_base)
elif '/connected_vehicle/soc' in simple_base:
- log.debug(f"DEBUG: Found connected_vehicle soc pattern, transforming: {simple_base}")
simple_base = re.sub(r'/get/connected_vehicle/soc$', '/soc', simple_base)
simple_base = re.sub(r'/connected_vehicle/soc$', '/soc', simple_base)
@@ -409,29 +413,40 @@ def _transform_chargepoint_topic(self, simple_base: str) -> Optional[str]:
# but exclude the ones we already transformed
if not re.search(r'/(chargemode|vehicle_name|soc)$', simple_base):
if re.search(r'/connected_vehicle/', simple_base):
- log.debug(f"DEBUG: Filtering out connected_vehicle topic: {simple_base}")
+ log.debug(f"Filtering out connected_vehicle topic: {simple_base}")
return None
- log.debug(f"DEBUG: _transform_chargepoint_topic output: {simple_base}")
+ log.debug(f"_transform_chargepoint_topic output: {simple_base}")
return simple_base
def _track_component_ids(self, topic: str):
- """Track component IDs to determine lowest IDs."""
- # Pattern to match topics with numeric IDs
- pattern = r'openWB/(\w+)/(\d+)/'
- match = re.match(pattern, topic)
-
+ """Track component IDs to determine lowest/all known IDs per type."""
+ match = re.match(r'openWB/(\w+)/(\d+)/', topic)
if match:
- component_type = match.group(1)
- component_id = int(match.group(2))
+ self._register_component_id(match.group(1), int(match.group(2)))
+ return
- if component_type not in self.lowest_ids:
- self.lowest_ids[component_type] = component_id
- else:
- self.lowest_ids[component_type] = min(self.lowest_ids[component_type], component_id)
+ # io IDs sit one level deeper (openWB/io/states//...)
+ io_match = re.match(r'openWB/io/states/(\d+)/', topic)
+ if io_match:
+ self._register_component_id('io', int(io_match.group(1)))
- def _expand_value_to_topics(self, base_topic: str, value: Any, result: Dict[str, Any]):
- """Expand complex values (JSON, tuples) into individual topics."""
+ def _register_component_id(self, component_type: str, component_id: int):
+ self.all_ids.setdefault(component_type, set()).add(component_id)
+ if component_type not in self.lowest_ids:
+ self.lowest_ids[component_type] = component_id
+ else:
+ self.lowest_ids[component_type] = min(self.lowest_ids[component_type], component_id)
+
+ def _expand_value_to_topics(
+ self, base_topic: str, value: Any, result: Dict[str, Any], already_transformed: bool = False):
+ """Expand complex values (JSON, tuples) into individual topics.
+
+ already_transformed is True only for the initial (non-recursive) call,
+ whose base_topic has already been run through _transform_chargepoint_topic
+ once by the caller - recursive calls append a new key/index suffix the
+ caller never saw, so those topics still need their own transform.
+ """
if isinstance(value, dict):
# Handle JSON objects
for key, val in value.items():
@@ -445,7 +460,7 @@ def _expand_value_to_topics(self, base_topic: str, value: Any, result: Dict[str,
else:
# Raw value - apply chargepoint transformations to final topics
final_topic = base_topic
- if '/chargepoint/' in base_topic:
+ if '/chargepoint/' in base_topic and not already_transformed:
transformed = self._transform_chargepoint_topic(base_topic)
if transformed is not None:
final_topic = transformed
@@ -472,12 +487,14 @@ def _generate_simplified_topics(self, simple_topic: str, parsed_value: Any) -> D
simplified_base = f"openWB/simpleAPI/{component_type}/{remaining_path}"
# Apply chargepoint transformations to simplified topics as well
+ already_transformed = False
if component_type == 'chargepoint':
simplified_base = self._transform_chargepoint_topic(simplified_base)
if simplified_base is None:
return result
+ already_transformed = True
- self._expand_value_to_topics(simplified_base, parsed_value, result)
+ self._expand_value_to_topics(simplified_base, parsed_value, result, already_transformed)
return result
@@ -520,111 +537,74 @@ def _cache_charge_template(self, topic: str, payload: str):
except Exception as e:
log.error(f"Error caching charge_template for {topic}: {e}")
+ # (path from charge_template root, key in that container, output field name,
+ # optional value transform) - the set of charge_template values that get
+ # mirrored out as simpleAPI read topics whenever a charge_template is cached.
+ _CHARGE_TEMPLATE_READ_FIELDS: Tuple[Tuple[Tuple[str, ...], str, str, Optional[Callable[[Any], Any]]], ...] = (
+ (('chargemode', 'pv_charging'), 'min_current', 'minimal_permanent_current', None),
+ (('chargemode', 'pv_charging'), 'min_soc', 'minimal_pv_soc', None),
+ # max_price is stored internally in EUR/Wh - published in ct/kWh
+ (('chargemode', 'eco_charging'), 'max_price', 'max_price_eco', lambda v: v * 100000),
+ (('chargemode', 'instant_charging', 'limit'), 'selected', 'instant_charging_limit', None),
+ # amount is stored internally in Wh - published in kWh
+ (('chargemode', 'instant_charging', 'limit'), 'amount', 'instant_charging_limit_amount', lambda v: v / 1000),
+ (('chargemode', 'instant_charging', 'limit'), 'soc', 'instant_charging_limit_soc', None),
+ (('chargemode', 'pv_charging', 'limit'), 'selected', 'pv_charging_limit', None),
+ (('chargemode', 'pv_charging', 'limit'), 'amount', 'pv_charging_limit_amount', lambda v: v / 1000),
+ (('chargemode', 'pv_charging', 'limit'), 'soc', 'pv_charging_limit_soc', None),
+ )
+
+ def _is_lowest_chargepoint(self, chargepoint_id: str) -> bool:
+ return 'chargepoint' in self.lowest_ids and self.lowest_ids['chargepoint'] == int(chargepoint_id)
+
+ def _publish_pair(self, chargepoint_id: str, field: str, value: Any):
+ """Publish a chargepoint-scoped simpleAPI topic, and also under the
+ ID-less alias if this chargepoint happens to be the lowest known one."""
+ self._publish_if_changed(f"openWB/simpleAPI/chargepoint/{chargepoint_id}/{field}", value)
+ if self._is_lowest_chargepoint(chargepoint_id):
+ self._publish_if_changed(f"openWB/simpleAPI/chargepoint/{field}", value)
+
def _publish_charge_template_read_topics(self, chargepoint_id: str, charge_template: Dict[str, Any]):
"""Publish readable topics for charge_template values."""
try:
- chargemode = charge_template.get('chargemode', {})
-
- # Extract min_current from pv_charging
- pv_charging = chargemode.get('pv_charging', {})
- if 'min_current' in pv_charging:
- topic = f"openWB/simpleAPI/chargepoint/{chargepoint_id}/minimal_permanent_current"
- self._publish_if_changed(topic, pv_charging['min_current'])
-
- # Also publish for lowest ID if this is it
- if 'chargepoint' in self.lowest_ids and self.lowest_ids['chargepoint'] == int(chargepoint_id):
- simple_topic = "openWB/simpleAPI/chargepoint/minimal_permanent_current"
- self._publish_if_changed(simple_topic, pv_charging['min_current'])
-
- # Extract min_soc from pv_charging (minimal_pv_soc)
- if 'min_soc' in pv_charging:
- topic = f"openWB/simpleAPI/chargepoint/{chargepoint_id}/minimal_pv_soc"
- self._publish_if_changed(topic, pv_charging['min_soc'])
-
- # Also publish for lowest ID if this is it
- if 'chargepoint' in self.lowest_ids and self.lowest_ids['chargepoint'] == int(chargepoint_id):
- simple_topic = "openWB/simpleAPI/chargepoint/minimal_pv_soc"
- self._publish_if_changed(simple_topic, pv_charging['min_soc'])
-
- # Extract max_price from eco_charging
- eco_charging = chargemode.get('eco_charging', {})
- if 'max_price' in eco_charging:
- topic = f"openWB/simpleAPI/chargepoint/{chargepoint_id}/max_price_eco"
- self._publish_if_changed(topic, eco_charging['max_price'])
-
- # Also publish for lowest ID if this is it
- if 'chargepoint' in self.lowest_ids and self.lowest_ids['chargepoint'] == int(chargepoint_id):
- simple_topic = "openWB/simpleAPI/chargepoint/max_price_eco"
- self._publish_if_changed(simple_topic, eco_charging['max_price'])
-
- # Extract instant_charging_limit values
- instant_charging = chargemode.get('instant_charging', {})
- limit = instant_charging.get('limit', {})
-
- if 'selected' in limit:
- topic = f"openWB/simpleAPI/chargepoint/{chargepoint_id}/instant_charging_limit"
- self._publish_if_changed(topic, limit['selected'])
-
- # Also publish for lowest ID if this is it
- if 'chargepoint' in self.lowest_ids and self.lowest_ids['chargepoint'] == int(chargepoint_id):
- simple_topic = "openWB/simpleAPI/chargepoint/instant_charging_limit"
- self._publish_if_changed(simple_topic, limit['selected'])
-
- if 'amount' in limit:
- # Convert from internal value (Wh) to kWh for display
- amount_kwh = limit['amount'] / 1000
- topic = f"openWB/simpleAPI/chargepoint/{chargepoint_id}/instant_charging_limit_amount"
- self._publish_if_changed(topic, amount_kwh)
-
- # Also publish for lowest ID if this is it
- if 'chargepoint' in self.lowest_ids and self.lowest_ids['chargepoint'] == int(chargepoint_id):
- simple_topic = "openWB/simpleAPI/chargepoint/instant_charging_limit_amount"
- self._publish_if_changed(simple_topic, amount_kwh)
-
- if 'soc' in limit:
- topic = f"openWB/simpleAPI/chargepoint/{chargepoint_id}/instant_charging_limit_soc"
- self._publish_if_changed(topic, limit['soc'])
-
- # Also publish for lowest ID if this is it
- if 'chargepoint' in self.lowest_ids and self.lowest_ids['chargepoint'] == int(chargepoint_id):
- simple_topic = "openWB/simpleAPI/chargepoint/instant_charging_limit_soc"
- self._publish_if_changed(simple_topic, limit['soc'])
-
- # Extract pv_charging_limit values
- pv_limit = pv_charging.get('limit', {})
-
- if 'selected' in pv_limit:
- topic = f"openWB/simpleAPI/chargepoint/{chargepoint_id}/pv_charging_limit"
- self._publish_if_changed(topic, pv_limit['selected'])
-
- # Also publish for lowest ID if this is it
- if 'chargepoint' in self.lowest_ids and self.lowest_ids['chargepoint'] == int(chargepoint_id):
- simple_topic = "openWB/simpleAPI/chargepoint/pv_charging_limit"
- self._publish_if_changed(simple_topic, pv_limit['selected'])
-
- if 'amount' in pv_limit:
- # Convert from internal value (Wh) to kWh for display
- amount_kwh = pv_limit['amount'] / 1000
- topic = f"openWB/simpleAPI/chargepoint/{chargepoint_id}/pv_charging_limit_amount"
- self._publish_if_changed(topic, amount_kwh)
-
- # Also publish for lowest ID if this is it
- if 'chargepoint' in self.lowest_ids and self.lowest_ids['chargepoint'] == int(chargepoint_id):
- simple_topic = "openWB/simpleAPI/chargepoint/pv_charging_limit_amount"
- self._publish_if_changed(simple_topic, amount_kwh)
-
- if 'soc' in pv_limit:
- topic = f"openWB/simpleAPI/chargepoint/{chargepoint_id}/pv_charging_limit_soc"
- self._publish_if_changed(topic, pv_limit['soc'])
-
- # Also publish for lowest ID if this is it
- if 'chargepoint' in self.lowest_ids and self.lowest_ids['chargepoint'] == int(chargepoint_id):
- simple_topic = "openWB/simpleAPI/chargepoint/pv_charging_limit_soc"
- self._publish_if_changed(simple_topic, pv_limit['soc'])
+ for path, key, field, transform in self._CHARGE_TEMPLATE_READ_FIELDS:
+ container = charge_template
+ for step in path:
+ container = container.get(step, {})
+ if key in container:
+ value = container[key]
+ if transform is not None:
+ value = transform(value)
+ self._publish_pair(chargepoint_id, field, value)
except Exception as e:
log.error(f"Error publishing charge_template read topics: {e}")
+ # Ordered (topic substring, handler method name) pairs for instant/pv charging
+ # limit set topics - checked in this order so the more specific suffixes
+ # (_soc, _amount) are matched before the bare limit type, which is itself a
+ # substring of both.
+ _LIMIT_OPERATIONS: Tuple[Tuple[str, str], ...] = (
+ ('instant_charging_limit_soc', '_handle_instant_charging_limit_soc_operation'),
+ ('instant_charging_limit_amount', '_handle_instant_charging_limit_amount_operation'),
+ ('instant_charging_limit', '_handle_instant_charging_limit_operation'),
+ ('pv_charging_limit_soc', '_handle_pv_charging_limit_soc_operation'),
+ ('pv_charging_limit_amount', '_handle_pv_charging_limit_amount_operation'),
+ ('pv_charging_limit', '_handle_pv_charging_limit_operation'),
+ )
+
+ def _parse_limit_topic_target(self, topic_remainder: str) -> Tuple[Optional[str], str]:
+ """Split a set-topic remainder into (explicit chargepoint ID or None, parameter name).
+
+ A limit topic arrives either as a bare parameter name (falls back to the
+ lowest known chargepoint ID) or as chargepoint// (explicit
+ ID) - mirrors the explicit-ID check in _handle_chargepoint_operation.
+ """
+ parts = topic_remainder.split('/')
+ if len(parts) >= 3 and parts[0] == 'chargepoint' and parts[1].isdigit():
+ return parts[1], parts[2]
+ return None, topic_remainder
+
def _handle_write_operation(self, topic: str, payload: str):
"""Handle write operations from simpleAPI set topics."""
try:
@@ -638,39 +618,15 @@ def _handle_write_operation(self, topic: str, payload: str):
# Parse the set topic to extract operation details
topic_remainder = topic.replace('openWB/simpleAPI/set/', '')
- # Check for instant_charging_limit operations first (can be with or without chargepoint ID)
- if 'instant_charging_limit_soc' in topic_remainder:
- success = self._handle_instant_charging_limit_soc_operation(payload)
- if success:
- self._clear_set_topic(topic)
- return
- elif 'instant_charging_limit_amount' in topic_remainder:
- success = self._handle_instant_charging_limit_amount_operation(payload)
- if success:
- self._clear_set_topic(topic)
- return
- elif 'instant_charging_limit' in topic_remainder:
- success = self._handle_instant_charging_limit_operation(payload)
- if success:
- self._clear_set_topic(topic)
- return
-
- # Check for pv_charging_limit operations first (can be with or without chargepoint ID)
- if 'pv_charging_limit_soc' in topic_remainder:
- success = self._handle_pv_charging_limit_soc_operation(payload)
- if success:
- self._clear_set_topic(topic)
- return
- elif 'pv_charging_limit_amount' in topic_remainder:
- success = self._handle_pv_charging_limit_amount_operation(payload)
- if success:
- self._clear_set_topic(topic)
- return
- elif 'pv_charging_limit' in topic_remainder:
- success = self._handle_pv_charging_limit_operation(payload)
- if success:
- self._clear_set_topic(topic)
- return
+ # Instant/pv charging limit operations (can be with or without an explicit chargepoint ID)
+ chargepoint_id, limit_param = self._parse_limit_topic_target(topic_remainder)
+ for substring, handler_name in self._LIMIT_OPERATIONS:
+ if substring in limit_param:
+ handler = getattr(self, handler_name)
+ success, _ = handler(payload, chargepoint_id)
+ if success:
+ self._clear_set_topic(topic)
+ return
topic_parts = topic_remainder.split('/')
@@ -685,11 +641,11 @@ def _handle_write_operation(self, topic: str, payload: str):
if success:
self._clear_set_topic(topic)
elif operation == 'bat_mode':
- success = self._handle_bat_mode_operation(payload)
+ success, _ = self._handle_bat_mode_operation(payload)
if success:
self._clear_set_topic(topic)
elif operation == 'bat_power_reserve':
- success = self._handle_bat_power_reserve_operation(payload)
+ success, _ = self._handle_bat_power_reserve_operation(payload)
if success:
self._clear_set_topic(topic)
else:
@@ -728,22 +684,22 @@ def _handle_chargepoint_operation(self, topic_parts: list, payload: str) -> bool
log.debug(f"Charge point operation: ID={chargepoint_id}, parameter={parameter}, value={payload}")
if parameter == 'chargemode':
- self._set_chargemode(chargepoint_id, payload)
+ success, _ = self._set_chargemode(chargepoint_id, payload)
elif parameter == 'chargecurrent':
- self._set_chargecurrent(chargepoint_id, payload)
+ success, _ = self._set_chargecurrent(chargepoint_id, payload)
elif parameter == 'minimal_pv_soc':
- self._set_minimal_pv_soc(chargepoint_id, payload)
+ success, _ = self._set_minimal_pv_soc(chargepoint_id, payload)
elif parameter == 'minimal_permanent_current':
- self._set_minimal_permanent_current(chargepoint_id, payload)
+ success, _ = self._set_minimal_permanent_current(chargepoint_id, payload)
elif parameter == 'max_price_eco':
- self._set_max_price_eco(chargepoint_id, payload)
+ success, _ = self._set_max_price_eco(chargepoint_id, payload)
elif parameter == 'chargepoint_lock':
- self._set_chargepoint_lock(chargepoint_id, payload)
+ success, _ = self._set_chargepoint_lock(chargepoint_id, payload)
else:
log.error(f"Unknown chargepoint parameter: {parameter}")
return False
- return True
+ return success
def _get_charge_template(self, chargepoint_id: str) -> Optional[Dict[str, Any]]:
"""Get charge_template for a chargepoint, either from cache or request it."""
@@ -753,7 +709,25 @@ def _get_charge_template(self, chargepoint_id: str) -> Optional[Dict[str, Any]]:
log.warning(f"No cached charge_template for chargepoint {chargepoint_id}")
return None
- def _set_chargemode(self, chargepoint_id: str, mode: str):
+ def _set_charge_template_field(
+ self, chargepoint_id: str, keypath: Tuple[str, ...], value: Any) -> Optional[Dict[str, Any]]:
+ """Fetch chargepoint_id's cached charge_template, set the nested field at
+ keypath, publish it back to openWB, and return the mutated template - or
+ None if no cached template is available (caller supplies the failure message)."""
+ charge_template = self._get_charge_template(chargepoint_id)
+ if charge_template is None:
+ return None
+
+ container = charge_template
+ for step in keypath[:-1]:
+ container = container[step]
+ container[keypath[-1]] = value
+
+ target_topic = f"openWB/set/chargepoint/{chargepoint_id}/set/charge_template"
+ self._publish_json(target_topic, charge_template)
+ return charge_template
+
+ def _set_chargemode(self, chargepoint_id: str, mode: str) -> Tuple[bool, str]:
"""Set chargemode for a chargepoint."""
# Mapping from simple values to internal values
mode_mapping = {
@@ -770,122 +744,120 @@ def _set_chargemode(self, chargepoint_id: str, mode: str):
}
if mode not in mode_mapping:
- log.error(f"Invalid chargemode: {mode}")
- return
+ message = f"Invalid chargemode: {mode}"
+ log.error(message)
+ return False, message
internal_mode = mode_mapping[mode]
- charge_template = self._get_charge_template(chargepoint_id)
-
- if charge_template is None:
- log.error(f"No charge_template available for chargepoint {chargepoint_id}")
- return
+ if self._set_charge_template_field(chargepoint_id, ('chargemode', 'selected'), internal_mode) is None:
+ message = f"No charge_template available for chargepoint {chargepoint_id}"
+ log.error(message)
+ return False, message
- # Modify the chargemode.selected value
- charge_template['chargemode']['selected'] = internal_mode
-
- # Publish the modified template
- target_topic = f"openWB/set/chargepoint/{chargepoint_id}/set/charge_template"
- self._publish_json(target_topic, charge_template)
log.info(f"Set chargemode to {mode} ({internal_mode}) for chargepoint {chargepoint_id}")
+ return True, f"Chargemode set to {mode} ({internal_mode})"
- def _set_chargecurrent(self, chargepoint_id: str, current: str):
+ def _set_chargecurrent(self, chargepoint_id: str, current: str) -> Tuple[bool, str]:
"""Set charge current for instant charging."""
try:
current_value = int(current)
- charge_template = self._get_charge_template(chargepoint_id)
-
- if charge_template is None:
- return
+ except ValueError:
+ message = f"Invalid current value: {current}"
+ log.error(message)
+ return False, message
- charge_template['chargemode']['instant_charging']['current'] = current_value
+ if self._set_charge_template_field(
+ chargepoint_id, ('chargemode', 'instant_charging', 'current'), current_value) is None:
+ message = f"No charge_template available for chargepoint {chargepoint_id}"
+ log.error(message)
+ return False, message
- target_topic = f"openWB/set/chargepoint/{chargepoint_id}/set/charge_template"
- self._publish_json(target_topic, charge_template)
- log.info(f"Set charge current to {current_value}A for chargepoint {chargepoint_id}")
+ log.info(f"Set charge current to {current_value}A for chargepoint {chargepoint_id}")
+ return True, f"Chargecurrent set to {current_value}A for chargepoint {chargepoint_id}"
- except ValueError:
- log.error(f"Invalid current value: {current}")
-
- def _set_minimal_pv_soc(self, chargepoint_id: str, soc: str):
+ def _set_minimal_pv_soc(self, chargepoint_id: str, soc: str) -> Tuple[bool, str]:
"""Set minimal EV SoC for PV charging."""
try:
soc_value = int(soc)
- charge_template = self._get_charge_template(chargepoint_id)
-
- if charge_template is None:
- return
-
- charge_template['chargemode']['pv_charging']['min_soc'] = soc_value
+ except ValueError:
+ message = f"Invalid SoC value: {soc}"
+ log.error(message)
+ return False, message
- target_topic = f"openWB/set/chargepoint/{chargepoint_id}/set/charge_template"
- self._publish_json(target_topic, charge_template)
- log.info(f"Set minimal PV SoC to {soc_value}% for chargepoint {chargepoint_id}")
+ if self._set_charge_template_field(
+ chargepoint_id, ('chargemode', 'pv_charging', 'min_soc'), soc_value) is None:
+ message = f"No charge_template available for chargepoint {chargepoint_id}"
+ log.error(message)
+ return False, message
- except ValueError:
- log.error(f"Invalid SoC value: {soc}")
+ log.info(f"Set minimal PV SoC to {soc_value}% for chargepoint {chargepoint_id}")
+ return True, f"Minimal PV SoC set to {soc_value}% for chargepoint {chargepoint_id}"
- def _set_minimal_permanent_current(self, chargepoint_id: str, current: str):
+ def _set_minimal_permanent_current(self, chargepoint_id: str, current: str) -> Tuple[bool, str]:
"""Set minimal permanent current for PV charging."""
try:
current_value = int(current)
- charge_template = self._get_charge_template(chargepoint_id)
-
- if charge_template is None:
- return
+ except ValueError:
+ message = f"Invalid current value: {current}"
+ log.error(message)
+ return False, message
- charge_template['chargemode']['pv_charging']['min_current'] = current_value
+ if self._set_charge_template_field(
+ chargepoint_id, ('chargemode', 'pv_charging', 'min_current'), current_value) is None:
+ message = f"No charge_template available for chargepoint {chargepoint_id}"
+ log.error(message)
+ return False, message
- target_topic = f"openWB/set/chargepoint/{chargepoint_id}/set/charge_template"
- self._publish_json(target_topic, charge_template)
- log.info(f"Set minimal permanent current to {current_value}A for chargepoint {chargepoint_id}")
+ log.info(f"Set minimal permanent current to {current_value}A for chargepoint {chargepoint_id}")
+ return True, f"Minimal permanent current set to {current_value}A for chargepoint {chargepoint_id}"
- except ValueError:
- log.error(f"Invalid current value: {current}")
-
- def _set_max_price_eco(self, chargepoint_id: str, price: str):
+ def _set_max_price_eco(self, chargepoint_id: str, price: str) -> Tuple[bool, str]:
"""Set maximum price for ECO charging."""
try:
price_value = float(price) / 100000
- charge_template = self._get_charge_template(chargepoint_id)
-
- if charge_template is None:
- return
-
- charge_template['chargemode']['eco_charging']['max_price'] = price_value
+ except ValueError:
+ message = f"Invalid price value: {price}"
+ log.error(message)
+ return False, message
- target_topic = f"openWB/set/chargepoint/{chargepoint_id}/set/charge_template"
- self._publish_json(target_topic, charge_template)
- log.info(f"Set max ECO price to {price_value} for chargepoint {chargepoint_id}")
+ if self._set_charge_template_field(
+ chargepoint_id, ('chargemode', 'eco_charging', 'max_price'), price_value) is None:
+ message = f"No charge_template available for chargepoint {chargepoint_id}"
+ log.error(message)
+ return False, message
- except ValueError:
- log.error(f"Invalid price value: {price}")
+ log.info(f"Set max ECO price to {price_value} for chargepoint {chargepoint_id}")
+ return True, f"Max price eco set to {price_value} for chargepoint {chargepoint_id}"
- def _set_chargepoint_lock(self, chargepoint_id: str, lock_state: str):
+ def _set_chargepoint_lock(self, chargepoint_id: str, lock_state: str) -> Tuple[bool, str]:
"""Set chargepoint lock state."""
try:
- lock_value = lock_state.lower() in ('true', '1', 'yes', 'on')
+ lock_value = str(lock_state).lower() in ('true', '1', 'yes', 'on')
target_topic = f"openWB/set/chargepoint/{chargepoint_id}/set/manual_lock"
self.client.publish(target_topic, str(lock_value).lower(), qos=0, retain=True)
log.info(f"Set chargepoint {chargepoint_id} lock to {lock_value}")
-
+ return True, f"Chargepoint lock set to {lock_value} for chargepoint {chargepoint_id}"
except Exception as e:
- log.error(f"Error setting chargepoint lock: {e}")
+ message = f"Error setting chargepoint lock: {e}"
+ log.error(message)
+ return False, message
- def _handle_bat_mode_operation(self, payload: str) -> bool:
+ def _handle_bat_mode_operation(self, payload: str) -> Tuple[bool, str]:
"""Handle battery mode operation."""
valid_modes = ['min_soc_bat_mode', 'ev_mode', 'bat_mode']
if payload not in valid_modes:
- log.error(f"Invalid bat_mode: {payload}. Valid values: {valid_modes}")
- return False
+ message = f"Invalid bat_mode: {payload}. Valid values: {valid_modes}"
+ log.error(message)
+ return False, message
target_topic = "openWB/set/general/chargemode_config/bat/mode"
self.client.publish(target_topic, payload, qos=0, retain=True)
log.info(f"Set bat_mode to {payload}")
- return True
+ return True, f"Bat mode set to {payload}"
- def _handle_bat_power_reserve_operation(self, payload: str) -> bool:
+ def _handle_bat_power_reserve_operation(self, payload: str) -> Tuple[bool, str]:
"""Handle battery power reserve operation.
Sets the power (in Watts) that openWB reserves for the battery
@@ -897,215 +869,154 @@ def _handle_bat_power_reserve_operation(self, payload: str) -> bool:
try:
watts = int(payload)
except ValueError:
- log.error(f"Invalid bat_power_reserve value: {payload!r}. Must be an integer (Watts).")
- return False
+ message = f"Invalid bat_power_reserve value: {payload!r}. Must be an integer (Watts)."
+ log.error(message)
+ return False, message
if watts < 0:
- log.error(f"Invalid bat_power_reserve value: {watts}. Must be >= 0.")
- return False
+ message = f"Invalid bat_power_reserve value: {watts}. Must be >= 0."
+ log.error(message)
+ return False, message
target_topic = "openWB/set/general/chargemode_config/bat/power_reserve"
self.client.publish(target_topic, watts, qos=0, retain=True)
log.info(f"Set bat_power_reserve to {watts}W")
- return True
-
- def _handle_instant_charging_limit_operation(self, payload: str) -> bool:
- """Handle instant charging limit type operation."""
- valid_limits = ['none', 'soc', 'amount']
-
- if payload not in valid_limits:
- log.error(f"Invalid instant_charging_limit: {payload}. Valid values: {valid_limits}")
- return False
+ return True, f"bat_power_reserve set to {watts}W"
- # Get chargepoint ID (use lowest if not specified)
+ def _resolve_chargepoint_id(self, chargepoint_id: Optional[str]) -> Optional[str]:
+ """Use the given chargepoint ID, or fall back to the lowest known one."""
+ if chargepoint_id is not None:
+ return str(chargepoint_id)
if 'chargepoint' in self.lowest_ids:
- chargepoint_id = str(self.lowest_ids['chargepoint'])
- else:
- log.error("No chargepoint ID found for instant_charging_limit operation")
- return False
-
- charge_template = self._get_charge_template(chargepoint_id)
- if charge_template is None:
- log.error(f"No charge_template available for chargepoint {chargepoint_id}")
- return False
-
- # Modify the instant_charging.limit.selected value
- charge_template['chargemode']['instant_charging']['limit']['selected'] = payload
+ return str(self.lowest_ids['chargepoint'])
+ return None
- # Publish the modified template
- target_topic = f"openWB/set/chargepoint/{chargepoint_id}/set/charge_template"
- self._publish_json(target_topic, charge_template)
- log.info(f"Set instant_charging_limit to {payload} for chargepoint {chargepoint_id}")
- return True
+ def _set_charging_limit_type(
+ self, mode_key: str, label: str, payload: str,
+ chargepoint_id: Optional[str] = None) -> Tuple[bool, str]:
+ """Shared body for instant_charging_limit / pv_charging_limit (the
+ chargemode..limit.selected field)."""
+ valid_limits = ['none', 'soc', 'amount']
- def _handle_instant_charging_limit_soc_operation(self, payload: str) -> bool:
- """Handle instant charging limit SoC operation."""
+ if payload not in valid_limits:
+ message = f"Invalid {mode_key}_limit: {payload}. Valid values: {valid_limits}"
+ log.error(message)
+ return False, message
+
+ chargepoint_id = self._resolve_chargepoint_id(chargepoint_id)
+ if chargepoint_id is None:
+ message = f"No chargepoint ID found for {mode_key}_limit operation"
+ log.error(message)
+ return False, message
+
+ if self._set_charge_template_field(
+ chargepoint_id, ('chargemode', mode_key, 'limit', 'selected'), payload) is None:
+ message = f"No charge_template available for chargepoint {chargepoint_id}"
+ log.error(message)
+ return False, message
+
+ log.info(f"Set {mode_key}_limit to {payload} for chargepoint {chargepoint_id}")
+ return True, f"{label} limit set to {payload} for chargepoint {chargepoint_id}"
+
+ def _set_charging_limit_soc(
+ self, mode_key: str, label: str, payload: str,
+ chargepoint_id: Optional[str] = None) -> Tuple[bool, str]:
+ """Shared body for instant_charging_limit_soc / pv_charging_limit_soc
+ (the chargemode..limit.soc field)."""
try:
soc_value = int(payload)
if soc_value < 0 or soc_value > 100:
- log.error(f"Invalid SoC value: {soc_value}. Must be between 0 and 100")
- return False
+ message = f"Invalid SoC value: {soc_value}. Must be between 0 and 100"
+ log.error(message)
+ return False, message
except ValueError:
- log.error(f"Invalid SoC value: {payload}. Must be an integer")
- return False
-
- # Get chargepoint ID (use lowest if not specified)
- if 'chargepoint' in self.lowest_ids:
- chargepoint_id = str(self.lowest_ids['chargepoint'])
- else:
- log.error("No chargepoint ID found for instant_charging_limit_soc operation")
- return False
-
- charge_template = self._get_charge_template(chargepoint_id)
- if charge_template is None:
- log.error(f"No charge_template available for chargepoint {chargepoint_id}")
- return False
-
- # Modify the instant_charging.limit.soc value
- charge_template['chargemode']['instant_charging']['limit']['soc'] = soc_value
-
- # Publish the modified template
- target_topic = f"openWB/set/chargepoint/{chargepoint_id}/set/charge_template"
- self._publish_json(target_topic, charge_template)
- log.info(f"Set instant_charging_limit_soc to {soc_value}% for chargepoint {chargepoint_id}")
- return True
-
- def _handle_instant_charging_limit_amount_operation(self, payload: str) -> bool:
- """Handle instant charging limit amount operation."""
+ message = f"Invalid SoC value: {payload}. Must be an integer"
+ log.error(message)
+ return False, message
+
+ chargepoint_id = self._resolve_chargepoint_id(chargepoint_id)
+ if chargepoint_id is None:
+ message = f"No chargepoint ID found for {mode_key}_limit_soc operation"
+ log.error(message)
+ return False, message
+
+ if self._set_charge_template_field(
+ chargepoint_id, ('chargemode', mode_key, 'limit', 'soc'), soc_value) is None:
+ message = f"No charge_template available for chargepoint {chargepoint_id}"
+ log.error(message)
+ return False, message
+
+ log.info(f"Set {mode_key}_limit_soc to {soc_value}% for chargepoint {chargepoint_id}")
+ return True, f"{label} SoC set to {soc_value}% for chargepoint {chargepoint_id}"
+
+ def _set_charging_limit_amount(
+ self, mode_key: str, label: str, payload: str,
+ chargepoint_id: Optional[str] = None) -> Tuple[bool, str]:
+ """Shared body for instant_charging_limit_amount / pv_charging_limit_amount
+ (the chargemode..limit.amount field, stored internally in Wh)."""
try:
amount_value = int(payload)
if amount_value < 1 or amount_value > 50:
- log.error(f"Invalid amount value: {amount_value}. Must be between 1 and 50")
- return False
-
- # Convert to internal value (multiply by 1000)
+ message = f"Invalid amount value: {amount_value}. Must be between 1 and 50"
+ log.error(message)
+ return False, message
internal_amount = amount_value * 1000
except ValueError:
- log.error(f"Invalid amount value: {payload}. Must be an integer")
- return False
+ message = f"Invalid amount value: {payload}. Must be an integer"
+ log.error(message)
+ return False, message
+
+ chargepoint_id = self._resolve_chargepoint_id(chargepoint_id)
+ if chargepoint_id is None:
+ message = f"No chargepoint ID found for {mode_key}_limit_amount operation"
+ log.error(message)
+ return False, message
+
+ if self._set_charge_template_field(
+ chargepoint_id, ('chargemode', mode_key, 'limit', 'amount'), internal_amount) is None:
+ message = f"No charge_template available for chargepoint {chargepoint_id}"
+ log.error(message)
+ return False, message
- # Get chargepoint ID (use lowest if not specified)
- if 'chargepoint' in self.lowest_ids:
- chargepoint_id = str(self.lowest_ids['chargepoint'])
- else:
- log.error("No chargepoint ID found for instant_charging_limit_amount operation")
- return False
-
- charge_template = self._get_charge_template(chargepoint_id)
- if charge_template is None:
- log.error(f"No charge_template available for chargepoint {chargepoint_id}")
- return False
-
- # Modify the instant_charging.limit.amount value
- charge_template['chargemode']['instant_charging']['limit']['amount'] = internal_amount
-
- # Publish the modified template
- target_topic = f"openWB/set/chargepoint/{chargepoint_id}/set/charge_template"
- self._publish_json(target_topic, charge_template)
log.info(
- f"Set instant_charging_limit_amount to {amount_value} kWh "
+ f"Set {mode_key}_limit_amount to {amount_value} kWh "
f"({internal_amount} Wh) for chargepoint {chargepoint_id}"
)
- return True
-
- def _handle_pv_charging_limit_operation(self, payload: str) -> bool:
- """Handle PV charging limit type operation."""
- valid_limits = ['none', 'soc', 'amount']
-
- if payload not in valid_limits:
- log.error(f"Invalid pv_charging_limit: {payload}. Valid values: {valid_limits}")
- return False
+ message = (
+ f"{label} amount set to {amount_value}kWh ({internal_amount}Wh) "
+ f"for chargepoint {chargepoint_id}"
+ )
+ return True, message
- # Get chargepoint ID (use lowest if not specified)
- if 'chargepoint' in self.lowest_ids:
- chargepoint_id = str(self.lowest_ids['chargepoint'])
- else:
- log.error("No chargepoint ID found for pv_charging_limit operation")
- return False
+ def _handle_instant_charging_limit_operation(
+ self, payload: str, chargepoint_id: Optional[str] = None) -> Tuple[bool, str]:
+ """Handle instant charging limit type operation."""
+ return self._set_charging_limit_type('instant_charging', 'Instant charging', payload, chargepoint_id)
- charge_template = self._get_charge_template(chargepoint_id)
- if charge_template is None:
- log.error(f"No charge_template available for chargepoint {chargepoint_id}")
- return False
+ def _handle_instant_charging_limit_soc_operation(
+ self, payload: str, chargepoint_id: Optional[str] = None) -> Tuple[bool, str]:
+ """Handle instant charging limit SoC operation."""
+ return self._set_charging_limit_soc('instant_charging', 'Instant charging', payload, chargepoint_id)
- # Modify the pv_charging.limit.selected value
- charge_template['chargemode']['pv_charging']['limit']['selected'] = payload
+ def _handle_instant_charging_limit_amount_operation(
+ self, payload: str, chargepoint_id: Optional[str] = None) -> Tuple[bool, str]:
+ """Handle instant charging limit amount operation."""
+ return self._set_charging_limit_amount('instant_charging', 'Instant charging', payload, chargepoint_id)
- # Publish the modified template
- target_topic = f"openWB/set/chargepoint/{chargepoint_id}/set/charge_template"
- self._publish_json(target_topic, charge_template)
- log.info(f"Set pv_charging_limit to {payload} for chargepoint {chargepoint_id}")
- return True
+ def _handle_pv_charging_limit_operation(
+ self, payload: str, chargepoint_id: Optional[str] = None) -> Tuple[bool, str]:
+ """Handle PV charging limit type operation."""
+ return self._set_charging_limit_type('pv_charging', 'PV charging', payload, chargepoint_id)
- def _handle_pv_charging_limit_soc_operation(self, payload: str) -> bool:
+ def _handle_pv_charging_limit_soc_operation(
+ self, payload: str, chargepoint_id: Optional[str] = None) -> Tuple[bool, str]:
"""Handle PV charging limit SoC operation."""
- try:
- soc_value = int(payload)
- if soc_value < 0 or soc_value > 100:
- log.error(f"Invalid SoC value: {soc_value}. Must be between 0 and 100")
- return False
- except ValueError:
- log.error(f"Invalid SoC value: {payload}. Must be an integer")
- return False
-
- # Get chargepoint ID (use lowest if not specified)
- if 'chargepoint' in self.lowest_ids:
- chargepoint_id = str(self.lowest_ids['chargepoint'])
- else:
- log.error("No chargepoint ID found for pv_charging_limit_soc operation")
- return False
+ return self._set_charging_limit_soc('pv_charging', 'PV charging', payload, chargepoint_id)
- charge_template = self._get_charge_template(chargepoint_id)
- if charge_template is None:
- log.error(f"No charge_template available for chargepoint {chargepoint_id}")
- return False
-
- # Modify the pv_charging.limit.soc value
- charge_template['chargemode']['pv_charging']['limit']['soc'] = soc_value
-
- # Publish the modified template
- target_topic = f"openWB/set/chargepoint/{chargepoint_id}/set/charge_template"
- self._publish_json(target_topic, charge_template)
- log.info(f"Set pv_charging_limit_soc to {soc_value}% for chargepoint {chargepoint_id}")
- return True
-
- def _handle_pv_charging_limit_amount_operation(self, payload: str) -> bool:
+ def _handle_pv_charging_limit_amount_operation(
+ self, payload: str, chargepoint_id: Optional[str] = None) -> Tuple[bool, str]:
"""Handle PV charging limit amount operation."""
- try:
- amount_value = int(payload)
- if amount_value < 1 or amount_value > 50:
- log.error(f"Invalid amount value: {amount_value}. Must be between 1 and 50")
- return False
-
- # Convert to internal value (multiply by 1000)
- internal_amount = amount_value * 1000
- except ValueError:
- log.error(f"Invalid amount value: {payload}. Must be an integer")
- return False
-
- # Get chargepoint ID (use lowest if not specified)
- if 'chargepoint' in self.lowest_ids:
- chargepoint_id = str(self.lowest_ids['chargepoint'])
- else:
- log.error("No chargepoint ID found for pv_charging_limit_amount operation")
- return False
-
- charge_template = self._get_charge_template(chargepoint_id)
- if charge_template is None:
- log.error(f"No charge_template available for chargepoint {chargepoint_id}")
- return False
-
- # Modify the pv_charging.limit.amount value
- charge_template['chargemode']['pv_charging']['limit']['amount'] = internal_amount
-
- # Publish the modified template
- target_topic = f"openWB/set/chargepoint/{chargepoint_id}/set/charge_template"
- self._publish_json(target_topic, charge_template)
- log.info(
- f"Set pv_charging_limit_amount to {amount_value} kWh "
- f"({internal_amount} Wh) for chargepoint {chargepoint_id}"
- )
- return True
+ return self._set_charging_limit_amount('pv_charging', 'PV charging', payload, chargepoint_id)
def _publish_json(self, topic: str, data: Dict[str, Any]):
"""Publish JSON data to a topic."""
@@ -1126,6 +1037,121 @@ def _publish_revision(self):
except Exception as e:
log.error(f"Failed to publish revision: {e}")
+ def _set_vehicle(self, chargepoint_id: str, value: str) -> Tuple[bool, str]:
+ """Assign a vehicle to a chargepoint."""
+ try:
+ vehicle_id = int(value)
+ except ValueError:
+ message = f"Invalid vehicle ID: {value}"
+ log.error(message)
+ return False, message
+
+ if vehicle_id < 0:
+ message = "Vehicle ID must be >= 0"
+ return False, message
+
+ topic = f"openWB/set/chargepoint/{chargepoint_id}/config/ev"
+ self.client.publish(topic, str(vehicle_id), qos=0, retain=True)
+ log.info(f"Assigned vehicle {vehicle_id} to chargepoint {chargepoint_id}")
+ return True, f"Vehicle {vehicle_id} assigned to chargepoint {chargepoint_id}"
+
+ def _set_manual_soc(self, chargepoint_id: str, value: str) -> Tuple[bool, str]:
+ """Set the manually entered SoC for the vehicle assigned to a chargepoint."""
+ try:
+ soc = int(value)
+ if soc < 0 or soc > 100:
+ message = f"Manual SoC must be between 0 and 100, got {soc}"
+ return False, message
+ except ValueError:
+ message = f"Invalid SoC value: {value}"
+ log.error(message)
+ return False, message
+
+ config = self.raw_value_cache.get(f"openWB/chargepoint/{chargepoint_id}/config")
+ vehicle_id = config.get('ev') if isinstance(config, dict) else None
+ if vehicle_id is None:
+ message = f"No vehicle assigned to chargepoint {chargepoint_id}"
+ log.error(message)
+ return False, message
+
+ topic = f"openWB/set/vehicle/{vehicle_id}/soc_module/calculated_soc_state/manual_soc"
+ self.client.publish(topic, str(soc), qos=0, retain=True)
+ log.info(f"Set manual SoC to {soc}% for vehicle {vehicle_id} (chargepoint {chargepoint_id})")
+ return True, f"Manual SoC set to {soc}% for vehicle {vehicle_id} (chargepoint {chargepoint_id})"
+
+ def _get_io_output_map(self, io_id: str, output_type: str) -> Dict[str, Any]:
+ value = self.raw_value_cache.get(f"openWB/io/states/{io_id}/get/{output_type}")
+ return value if isinstance(value, dict) else {}
+
+ @staticmethod
+ def _normalize_io_output_type(output_type: Optional[str]) -> Optional[str]:
+ if output_type is None or str(output_type).strip() == '':
+ return None
+ value = str(output_type).strip().lower()
+ if value in ('digital', 'digital_output'):
+ return 'digital_output'
+ if value in ('analog', 'analog_output'):
+ return 'analog_output'
+ return None
+
+ @staticmethod
+ def _parse_io_output_value(value: Any):
+ if isinstance(value, bool):
+ return value
+ if isinstance(value, (int, float)):
+ return float(value)
+ normalized = str(value).strip().lower()
+ if normalized in ('true', 'on', 'yes'):
+ return True
+ if normalized in ('false', 'off', 'no'):
+ return False
+ try:
+ return float(normalized)
+ except ValueError:
+ return value
+
+ def _set_io_output(
+ self, io_id: Optional[str], value: str, output_name: Optional[str],
+ output_type: Optional[str] = None) -> Tuple[bool, str]:
+ """Manually set a digital or analog IO output."""
+ if io_id is None or str(io_id) == '':
+ return False, "Missing io_nr"
+ if output_name is None or str(output_name).strip() == '':
+ return False, "Missing io_output"
+
+ output_name = str(output_name).strip()
+ normalized_type = self._normalize_io_output_type(output_type)
+ parsed_value = self._parse_io_output_value(value)
+
+ if normalized_type is None:
+ digital = self._get_io_output_map(io_id, 'digital_output')
+ analog = self._get_io_output_map(io_id, 'analog_output')
+ if output_name in digital:
+ normalized_type = 'digital_output'
+ elif output_name in analog:
+ normalized_type = 'analog_output'
+ elif isinstance(parsed_value, bool):
+ normalized_type = 'digital_output'
+ else:
+ normalized_type = 'analog_output'
+
+ if normalized_type == 'digital_output':
+ if isinstance(parsed_value, bool):
+ payload = 'true' if parsed_value else 'false'
+ elif isinstance(parsed_value, (int, float)) and float(parsed_value) == 0.0:
+ payload = 'false'
+ elif isinstance(parsed_value, (int, float)) and float(parsed_value) == 1.0:
+ payload = 'true'
+ else:
+ return False, "Digital output expects boolean or 0/1"
+ else:
+ payload = '1' if parsed_value is True else '0' if parsed_value is False else str(parsed_value)
+
+ topic = f"openWB/set/system/io/{io_id}/set/manual/{normalized_type}/{output_name}"
+ self.client.publish(topic, payload, qos=0, retain=True)
+ log.info(f"Set {normalized_type}/{output_name} to {payload} for io {io_id}")
+ return True, f"Set {normalized_type}/{output_name} to {payload} for io {io_id}"
+
def connect(self):
"""Establish connection to MQTT broker."""
try:
@@ -1150,18 +1176,24 @@ def connect(self):
return False
def run(self):
- """Start the daemon main loop."""
+ """Start the MQTT client's own thread and run the HTTP server on the main thread."""
if not self.connect():
sys.exit(1)
- log.info("SimpleMQTT API Daemon started")
+ self.client.loop_start()
+ log.info(
+ f"SimpleMQTT API Daemon started, HTTP server listening on "
+ f"{self.http_server.server_address[0]}:{self.http_server.server_address[1]}"
+ )
try:
- self.client.loop_forever()
+ self.http_server.serve_forever()
except KeyboardInterrupt:
log.info("Received interrupt signal, shutting down...")
+ finally:
+ self.http_server.shutdown()
+ self.client.loop_stop()
self.client.disconnect()
- sys.exit(0)
def main():
diff --git a/simpleAPI/src/Authenticator.php b/simpleAPI/src/Authenticator.php
deleted file mode 100644
index a93af05052..0000000000
--- a/simpleAPI/src/Authenticator.php
+++ /dev/null
@@ -1,212 +0,0 @@
-config = $config;
- $this->mqttClient = $mqttClient;
- }
-
- /**
- * Authentifizierung prüfen
- */
- public function authenticate($params)
- {
- // Wenn keine Authentifizierung erforderlich ist
- if (!$this->isAuthRequired()) {
- return true;
- }
-
- // Bearer Token prüfen
- if ($this->checkBearerToken()) {
- return true;
- }
-
- // Username/Password prüfen
- if ($this->checkUsernamePassword($params)) {
- return true;
- }
-
- // Token aus Parameter prüfen
- if ($this->checkParameterToken($params)) {
- return true;
- }
-
- // Fallback: Prüfen ob MQTT Broker ohne Authentifizierung zugänglich ist
- if ($this->checkMqttNoAuth()) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Prüfen ob Authentifizierung erforderlich ist
- */
- private function isAuthRequired()
- {
- return $this->config['auth']['enabled'] ?? false;
- }
-
- /**
- * Bearer Token aus Authorization Header prüfen
- */
- private function checkBearerToken()
- {
- $headers = $this->getAuthorizationHeader();
-
- if (!$headers) {
- return false;
- }
-
- // Bearer Token extrahieren
- if (preg_match('/Bearer\s+(.*)$/i', $headers, $matches)) {
- $token = $matches[1];
- return $this->validateToken($token);
- }
-
- return false;
- }
-
- /**
- * Username/Password aus POST-Parametern prüfen
- */
- private function checkUsernamePassword($params)
- {
- if (!isset($params['username']) || !isset($params['password'])) {
- return false;
- }
-
- $username = $params['username'];
- $password = $params['password'];
-
- // Nur bei HTTPS erlaubt
- if (!$this->isHttps() && $this->config['auth']['require_https']) {
- return false;
- }
-
- return $this->validateCredentials($username, $password);
- }
-
- /**
- * Token aus Parameter prüfen
- */
- private function checkParameterToken($params)
- {
- if (!isset($params['token'])) {
- return false;
- }
-
- // Nur bei HTTPS erlaubt
- if (!$this->isHttps() && $this->config['auth']['require_https']) {
- return false;
- }
-
- return $this->validateToken($params['token']);
- }
-
- /**
- * Token validieren
- */
- private function validateToken($token)
- {
- $validTokens = $this->config['auth']['tokens'] ?? [];
-
- foreach ($validTokens as $validToken) {
- if (hash_equals($validToken, $token)) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Benutzerdaten validieren
- */
- private function validateCredentials($username, $password)
- {
- $users = $this->config['auth']['users'] ?? [];
-
- // Zuerst gegen definierte Benutzer prüfen
- if (!empty($users) && isset($users[$username])) {
- $storedPassword = $users[$username];
-
- // Passwort-Hash prüfen
- if (strpos($storedPassword, '$') === 0) {
- // Gehashtes Passwort
- return password_verify($password, $storedPassword);
- } else {
- // Klartext (nicht empfohlen)
- return hash_equals($storedPassword, $password);
- }
- }
-
- // Wenn keine lokalen Benutzer definiert sind oder MqttClient verfügbar ist:
- // Anmeldedaten gegen MQTT Broker testen
- if ($this->mqttClient && (empty($users) || !isset($users[$username]))) {
- return $this->mqttClient->testCredentials($username, $password);
- }
-
- return false;
- }
-
- /**
- * Authorization Header abrufen
- */
- private function getAuthorizationHeader()
- {
- $headers = null;
-
- if (isset($_SERVER['Authorization'])) {
- $headers = trim($_SERVER['Authorization']);
- } elseif (isset($_SERVER['HTTP_AUTHORIZATION'])) {
- $headers = trim($_SERVER['HTTP_AUTHORIZATION']);
- } elseif (function_exists('apache_request_headers')) {
- $requestHeaders = apache_request_headers();
- $requestHeaders = array_combine(
- array_map('ucwords', array_keys($requestHeaders)),
- array_values($requestHeaders)
- );
-
- if (isset($requestHeaders['Authorization'])) {
- $headers = trim($requestHeaders['Authorization']);
- }
- }
-
- return $headers;
- }
-
- /**
- * Prüfen ob HTTPS verwendet wird
- */
- private function isHttps()
- {
- return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
- $_SERVER['SERVER_PORT'] == 443 ||
- (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https');
- }
-
- /**
- * Prüfen ob MQTT Broker ohne Authentifizierung zugänglich ist
- */
- private function checkMqttNoAuth()
- {
- // Nur wenn MqttClient verfügbar ist
- if (!$this->mqttClient) {
- return false;
- }
-
- // Test-Verbindung ohne Anmeldedaten
- return $this->mqttClient->testCredentials('', '');
- }
-}
diff --git a/simpleAPI/src/MqttClient.php b/simpleAPI/src/MqttClient.php
deleted file mode 100644
index d715f195b7..0000000000
--- a/simpleAPI/src/MqttClient.php
+++ /dev/null
@@ -1,359 +0,0 @@
-server = $config['mqtt']['server'] ?? 'localhost';
- $this->port = $config['mqtt']['port'] ?? 1884;
- $this->validate_cert = $config['mqtt']['validate_cert'] ?? false;
- $this->username = $config['mqtt']['username'] ?? '';
- $this->password = $config['mqtt']['password'] ?? '';
- $this->clientid = $config['mqtt']['clientid'] ?? 'SimpleAPI_' . uniqid();
- }
-
- /**
- * MQTT-Anmeldedaten aktualisieren
- */
- public function setCredentials($username, $password)
- {
- $this->username = $username;
- $this->password = $password;
- }
-
- /**
- * Verbindung testen
- */
- public function connect()
- {
- // Test-Verbindung mit mosquitto_sub
- $cmd = $this->buildMosquittoCommand('sub', 'test/connection', '', 1);
- $result = shell_exec($cmd . ' 2>&1');
-
- // Wenn kein Fehler zurückkommt, ist die Verbindung OK
- return !preg_match('/error|failed|unable/i', $result ?? '');
- }
-
- /**
- * Anmeldedaten am MQTT Broker testen
- */
- public function testCredentials($username, $password)
- {
- // Test-Verbindung mit mosquitto_sub (sehr kurzes Timeout)
- $cmd = $this->buildMosquittoCommandWithCredentials('sub', '$SYS/broker/uptime', '', 1, 2, $username, $password);
- $result = shell_exec($cmd . ' 2>&1');
-
- // Prüfen auf Authentifizierungsfehler
- if (preg_match('/not authorised|connection refused|authentication failed|username or password invalid/i', $result ?? '')) {
- return false;
- }
-
- // Prüfen auf andere schwerwiegende Fehler
- if (preg_match('/connection error|network unreachable|no such host/i', $result ?? '')) {
- return false;
- }
-
- return true;
- }
-
- /**
- * Wert aus MQTT Topic lesen
- */
- public function getValue($topic)
- {
- $cmd = $this->buildMosquittoCommand('sub', $topic, '', 1);
- $output = shell_exec($cmd . ' 2>/dev/null');
- $value = trim($output ?? '');
-
- if ($value === '') {
- throw new \Exception("No data received for topic: $topic");
- }
-
- // Bei mosquitto_sub mit -v wird das Format "topic value" ausgegeben
- // Wir müssen das Topic-Präfix entfernen
- if (strpos($value, $topic . ' ') === 0) {
- $value = substr($value, strlen($topic . ' '));
- }
-
- return $value;
- }
-
- /**
- * Mehrere Topics gleichzeitig abfragen (Performance-Optimierung)
- */
- public function getMultipleValues($topics)
- {
- $results = [];
-
- $cmd = $this->buildMosquittoCommand('sub', $topics, '', count($topics));
- $cmd .= ' 2>/dev/null';
-
- $output = shell_exec($cmd);
- $lines = explode("\n", trim($output ?? ''));
-
- foreach ($lines as $line) {
- if (strpos($line, ' ') !== false) {
- list($topic, $value) = explode(' ', $line, 2);
- $results[$topic] = $value;
- }
- }
-
- // Nur Topics zurückgeben, die erfolgreich abgerufen wurden
- return $results;
- }
-
- /**
- * Alle Werte für ein MQTT-Wildcard-Topic lesen.
- */
- public function getValuesByWildcard($topicPattern)
- {
- $results = [];
-
- $cmd = $this->buildMosquittoCommand('sub', $topicPattern, '');
- $cmd .= ' 2>/dev/null';
-
- $output = shell_exec($cmd);
- $lines = explode("\n", trim($output ?? ''));
-
- foreach ($lines as $line) {
- if (strpos($line, ' ') !== false) {
- list($topic, $value) = explode(' ', $line, 2);
- $results[$topic] = $value;
- }
- }
-
- return $results;
- }
-
- /**
- * Wert in MQTT Topic schreiben
- */
- public function setValue($topic, $value)
- {
- $cmd = $this->buildMosquittoCommand('pub', $topic, $value);
- $result = shell_exec($cmd . ' 2>&1');
-
- // Prüfen ob Fehler aufgetreten sind
- if (preg_match('/error|failed|unable/i', $result ?? '')) {
- throw new \Exception("Failed to publish to topic: $topic - $result");
- }
-
- return true;
- }
-
- /**
- * Mosquitto-Kommando erstellen
- */
- private function buildMosquittoCommand($type, $topics, $message = '', $count = null, $timeout = 1, $extraArgs = [])
- {
- return $this->buildMosquittoCommandWithCredentials($type, $topics, $message, $count, $timeout, $this->username, $this->password, $extraArgs);
- }
-
- /**
- * Mosquitto-Kommando mit spezifischen Anmeldedaten erstellen
- */
- private function buildMosquittoCommandWithCredentials($type, $topics, $message = '', $count = null, $timeout = 1, $username = null, $password = null, $extraArgs = [])
- {
- $binary = $type === 'sub' ? 'mosquitto_sub' : 'mosquitto_pub';
-
- $cmd = sprintf(
- "%s -h %s -p %d",
- $binary,
- escapeshellarg($this->server),
- $this->port
- );
-
- // Zertifikatsvalidierung deaktivieren falls konfiguriert
- if (!$this->validate_cert) {
- $cmd .= " --insecure";
- }
-
- // Username/Passwort hinzufügen falls konfiguriert
- $useUsername = $username !== null ? $username : $this->username;
- $usePassword = $password !== null ? $password : $this->password;
-
- if (!empty($useUsername)) {
- $cmd .= sprintf(" -u %s", escapeshellarg($useUsername));
- }
- if (!empty($usePassword)) {
- $cmd .= sprintf(" -P %s", escapeshellarg($usePassword));
- }
-
- // Topic(s) hinzufügen
- if (is_array($topics)) {
- foreach ($topics as $topic) {
- $cmd .= sprintf(" -t %s", escapeshellarg($topic));
- }
- } else {
- $cmd .= sprintf(" -t %s", escapeshellarg($topics));
- }
-
- // Count und Timeout für subscribe
- if ($type === 'sub') {
- // Ausgabe des Topics erzwingen
- $cmd .= " -v";
- if ($count !== null) {
- $cmd .= sprintf(" -C %d", intval($count));
- }
- if ($timeout !== null) {
- $cmd .= sprintf(" -W %d", intval($timeout));
- }
- }
-
- // Message für publish
- if ($type === 'pub' && $message !== '') {
- $cmd .= sprintf(" -m %s", escapeshellarg($message));
- }
-
- // Extra-Argumente hinzufügen
- foreach ($extraArgs as $arg) {
- $cmd .= " " . $arg;
- }
-
- return $cmd;
- }
-
- /**
- * Scan-Konfiguration je Typ: welches Wildcard-Topic zuverlässig für jedes konfigurierte
- * Gerät dieses Typs publiziert wird, und wie die ID daraus extrahiert wird.
- */
- private function getScanConfig()
- {
- return [
- 'chargepoint' => ['pattern' => 'openWB/chargepoint/+/get/imported', 'regex' => '/openWB\/chargepoint\/(\d+)\/get\/imported\s+(.+)/'],
- 'bat' => ['pattern' => 'openWB/bat/+/get/imported', 'regex' => '/openWB\/bat\/(\d+)\/get\/imported\s+(.+)/'],
- 'pv' => ['pattern' => 'openWB/pv/+/get/exported', 'regex' => '/openWB\/pv\/(\d+)\/get\/exported\s+(.+)/'],
- 'counter' => ['pattern' => 'openWB/counter/+/get/imported', 'regex' => '/openWB\/counter\/(\d+)\/get\/imported\s+(.+)/'],
- 'consumer' => ['pattern' => 'openWB/consumer/+/module', 'regex' => '/openWB\/consumer\/(\d+)\/module\s+(.+)/'],
- 'io' => ['pattern' => 'openWB/io/states/+/get/digital_output', 'regex' => '/openWB\/io\/states\/(\d+)\/get\/digital_output\s+(.+)/']
- ];
- }
-
- /**
- * Alle verfügbaren IDs für einen bestimmten Typ finden (MQTT Wildcard Scan).
- * Wirft eine Exception, wenn keine Geräte gefunden wurden (genutzt von getLowestId, wo ein
- * leeres Ergebnis ein echter Fehlerfall ist).
- */
- public function findAvailableIds($type)
- {
- $ids = $this->scanAvailableIds($type);
-
- if (empty($ids)) {
- throw new \Exception("No {$type} devices found via MQTT wildcard scan");
- }
-
- return $ids;
- }
-
- /**
- * Wie findAvailableIds(), aber ohne Exception bei leerem Ergebnis - für list_components, wo
- * ein Typ ohne konfigurierte Geräte kein Fehler, sondern schlicht eine leere Liste ist.
- */
- private function scanAvailableIds($type)
- {
- $config = $this->getScanConfig()[$type] ?? null;
- if ($config === null) {
- throw new \Exception("Unsupported type for ID scan: {$type}");
- }
-
- $pattern = $config['pattern'];
- $matchRegex = $config['regex'];
-
- $cmd = $this->buildMosquittoCommand('sub', $pattern, '');
- $cmd .= ' 2>/dev/null';
-
- $output = shell_exec($cmd);
- $ids = [];
-
- if ($output) {
- $lines = explode("\n", trim($output));
- foreach ($lines as $line) {
- if (preg_match($matchRegex, $line, $matches)) {
- $id = intval($matches[1]);
- $value = trim($matches[2]);
-
- // Nur IDs mit gültigen Werten (bei IO/Consumer auch JSON erlaubt)
- if ($value !== '' && $value !== 'null' &&
- ($type === 'io' || $type === 'consumer' || is_numeric($value))) {
- $ids[] = $id;
- }
- }
- }
- }
-
- // array_unique() zuerst (behält Original-Keys, ggf. lückenhaft), dann sort() zum
- // Schluss, da sort() die Keys wieder lückenlos ab 0 durchnummeriert - sonst würde
- // json_encode() bei entfernten Duplikaten ein Objekt statt ein Array ausgeben.
- $ids = array_unique($ids);
- sort($ids, SORT_NUMERIC);
- return $ids;
- }
-
- /**
- * Alle verfügbaren IDs für alle bekannten Typen auf einmal finden (für list_components).
- * Typen ohne konfigurierte Geräte liefern eine leere Liste statt einer Exception.
- */
- public function findAllAvailableIds()
- {
- $result = [];
- foreach (array_keys($this->getScanConfig()) as $type) {
- // Öffentlicher Name: 'bat' intern, aber 'battery' in Parametern/Antworten üblich.
- $publicName = $type === 'bat' ? 'battery' : $type;
- $result[$publicName] = $this->scanAvailableIds($type);
- }
- return $result;
- }
-
- /**
- * Niedrigste verfügbare ID für einen Typ finden (Dynamische Erkennung)
- */
- public function getLowestId($type)
- {
- // Cache prüfen
- if (isset(self::$knownIds[$type])) {
- return self::$knownIds[$type];
- }
-
- // Alle verfügbaren IDs für diesen Typ finden
- $availableIds = $this->findAvailableIds($type);
-
- if (!empty($availableIds)) {
- // Niedrigste ID zurückgeben
- sort($availableIds, SORT_NUMERIC);
- $lowestId = $availableIds[0];
- self::$knownIds[$type] = $lowestId;
- return $lowestId;
- }
-
- return null;
- }
-
- /**
- * Verbindung schließen (dummy für Kompatibilität)
- */
- public function disconnect()
- {
- // Nicht nötig bei mosquitto_sub/pub
- }
-
- public function __destruct()
- {
- $this->disconnect();
- }
-}
diff --git a/simpleAPI/src/MqttClientSimple.php b/simpleAPI/src/MqttClientSimple.php
deleted file mode 100644
index 26848fbee2..0000000000
--- a/simpleAPI/src/MqttClientSimple.php
+++ /dev/null
@@ -1,159 +0,0 @@
-server = $config['mqtt']['server'] ?? 'localhost';
- $this->port = $config['mqtt']['port'] ?? 1883;
- $this->username = $config['mqtt']['username'] ?? '';
- $this->password = $config['mqtt']['password'] ?? '';
- $this->clientid = $config['mqtt']['clientid'] ?? 'SimpleAPI_' . uniqid();
- }
-
- /**
- * Verbindung testen
- */
- public function connect()
- {
- // Test-Verbindung mit mosquitto_sub
- $cmd = $this->buildMosquittoCommand('sub', 'test/connection', '', ['-C', '1', '-W', '1']);
- $result = shell_exec($cmd . ' 2>&1');
-
- // Wenn kein Fehler zurückkommt, ist die Verbindung OK
- return !preg_match('/error|failed|unable/i', $result ?? '');
- }
-
- /**
- * Wert aus MQTT Topic lesen
- */
- public function getValue($topic)
- {
- $cmd = $this->buildMosquittoCommand('sub', $topic, '', ['-C', '1', '-W', '3']);
- $output = shell_exec($cmd . ' 2>/dev/null');
- $value = trim($output ?? '');
-
- if ($value === '') {
- throw new \Exception("No data received for topic: $topic");
- }
-
- return $value;
- }
-
- /**
- * Wert in MQTT Topic schreiben
- */
- public function setValue($topic, $value)
- {
- $cmd = $this->buildMosquittoCommand('pub', $topic, $value);
- $result = shell_exec($cmd . ' 2>&1');
-
- // Prüfen ob Fehler aufgetreten sind
- if (preg_match('/error|failed|unable/i', $result ?? '')) {
- throw new \Exception("Failed to publish to topic: $topic - $result");
- }
-
- return true;
- }
-
- /**
- * Mosquitto-Kommando erstellen
- */
- private function buildMosquittoCommand($type, $topic, $message = '', $extraArgs = [])
- {
- $binary = $type === 'sub' ? 'mosquitto_sub' : 'mosquitto_pub';
-
- $cmd = sprintf(
- "%s -h %s -p %d",
- $binary,
- escapeshellarg($this->server),
- $this->port
- );
-
- // Username/Passwort hinzufügen falls konfiguriert
- if (!empty($this->username)) {
- $cmd .= sprintf(" -u %s", escapeshellarg($this->username));
- }
- if (!empty($this->password)) {
- $cmd .= sprintf(" -P %s", escapeshellarg($this->password));
- }
-
- // Topic hinzufügen
- $cmd .= sprintf(" -t %s", escapeshellarg($topic));
-
- // Message für publish
- if ($type === 'pub' && $message !== '') {
- $cmd .= sprintf(" -m %s", escapeshellarg($message));
- }
-
- // Extra-Argumente hinzufügen
- foreach ($extraArgs as $arg) {
- $cmd .= " " . $arg;
- }
-
- return $cmd;
- }
-
- /**
- * Alle verfügbaren IDs für einen bestimmten Typ finden
- */
- public function findAvailableIds($type)
- {
- $ids = [];
- $maxIds = $type === 'chargepoint' ? 8 : 10;
-
- for ($i = 0; $i <= $maxIds; $i++) {
- try {
- $testTopic = "openWB/{$type}/{$i}/get/power";
- $value = $this->getValue($testTopic);
-
- if ($value !== null && $value !== '') {
- $ids[] = $i;
- }
- } catch (\Exception $e) {
- // ID nicht verfügbar, weiter
- continue;
- }
- }
-
- if (empty($ids)) {
- throw new \Exception("No {$type} devices found via MQTT");
- }
-
- return $ids;
- }
-
- /**
- * Niedrigste verfügbare ID für einen Typ finden
- */
- public function getLowestId($type)
- {
- $ids = $this->findAvailableIds($type);
- return min($ids);
- }
-
- /**
- * Verbindung schließen (dummy für Kompatibilität)
- */
- public function disconnect()
- {
- // Nicht nötig bei mosquitto_sub/pub
- }
-
- public function __destruct()
- {
- $this->disconnect();
- }
-}
diff --git a/simpleAPI/src/ParameterHandler.php b/simpleAPI/src/ParameterHandler.php
deleted file mode 100644
index 8e886b8167..0000000000
--- a/simpleAPI/src/ParameterHandler.php
+++ /dev/null
@@ -1,2986 +0,0 @@
-mqttClient = $mqttClient;
- }
-
- /**
- * Parameter lesen
- */
- public function readParameter($param, $id, $options = [])
- {
- switch ($param) {
- case 'get_chargepoint_all':
- return $this->getChargepointAll($id);
-
- case 'get_chargepoint_voltage_p1':
- return $this->getChargepointVoltage($id, 1);
-
- case 'get_chargepoint_voltage_p2':
- return $this->getChargepointVoltage($id, 2);
-
- case 'get_chargepoint_voltage_p3':
- return $this->getChargepointVoltage($id, 3);
-
- case 'get_chargepoint_voltages':
- return $this->getChargepointVoltages($id);
-
- case 'get_chargepoint_current_p1':
- return $this->getChargepointCurrent($id, 1);
-
- case 'get_chargepoint_current_p2':
- return $this->getChargepointCurrent($id, 2);
-
- case 'get_chargepoint_current_p3':
- return $this->getChargepointCurrent($id, 3);
-
- case 'get_chargepoint_currents':
- return $this->getChargepointCurrents($id);
-
- case 'get_chargepoint_power':
- return $this->getChargepointPower($id);
-
- case 'get_chargepoint_powers':
- return $this->getChargepointPowers($id);
-
- case 'battery':
- return $this->getBattery($id);
-
- case 'pv':
- return $this->getPv($id);
-
- case 'get_counter':
- return $this->getCounter($id);
-
- // Chargepoint - Einzelwerte
- case 'get_chargepoint_imported':
- return $this->getChargepointImported($id);
- case 'get_chargepoint_exported':
- return $this->getChargepointExported($id);
- case 'get_chargepoint_daily_imported':
- return $this->getChargepointDailyImported($id);
- case 'get_chargepoint_daily_exported':
- return $this->getChargepointDailyExported($id);
- case 'get_chargepoint_monthly_exported':
- return $this->getChargepointMonthlyExported($id);
- case 'get_chargepoint_yearly_exported':
- return $this->getChargepointYearlyExported($id);
- case 'get_chargepoint_frequency':
- return $this->getChargepointFrequency($id);
- case 'get_chargepoint_rfid':
- return $this->getChargepointRfid($id);
- case 'get_chargepoint_rfid_timestamp':
- return $this->getChargepointRfidTimestamp($id);
- case 'get_chargepoint_evse_current':
- return $this->getChargepointEvseCurrent($id);
- case 'get_chargepoint_power_factors':
- return $this->getChargepointPowerFactors($id);
- case 'get_chargepoint_power_factor_p1':
- return $this->getChargepointPowerFactor($id, 1);
- case 'get_chargepoint_power_factor_p2':
- return $this->getChargepointPowerFactor($id, 2);
- case 'get_chargepoint_power_factor_p3':
- return $this->getChargepointPowerFactor($id, 3);
- case 'get_chargepoint_config_name':
- return $this->getChargepointConfigName($id);
- case 'get_chargepoint_connected_vehicle_name':
- return $this->getChargepointConnectedVehicleName($id);
- case 'get_chargepoint_charge_template_name':
- return $this->getChargepointChargeTemplateName($id);
- case 'get_chargepoint_charge_template_min_current':
- return $this->getChargepointChargeTemplateMinCurrent($id);
- case 'get_chargepoint_instant_charging_current':
- return $this->getChargepointInstantChargingCurrent($id);
- case 'get_chargepoint_pv_charging_min_current':
- return $this->getChargepointPvChargingMinCurrent($id);
- case 'get_chargepoint_soc':
- return $this->getChargepointSoc($id);
- case 'get_chargepoint_state_str':
- return $this->getChargepointStateStr($id);
- case 'get_chargepoint_fault_str':
- return $this->getChargepointFaultStr($id);
- case 'get_chargepoint_fault_state':
- return $this->getChargepointFaultState($id);
- case 'get_chargepoint_phases_in_use':
- return $this->getChargepointPhasesInUse($id);
- case 'get_chargepoint_plug_state':
- return $this->getChargepointPlugState($id);
- case 'get_chargepoint_charge_state':
- return $this->getChargepointChargeState($id);
- case 'get_chargepoint_chargemode':
- return $this->getChargepointChargemode($id);
-
- // Counter - Einzelwerte
- case 'get_counter_voltage_p1':
- return $this->getCounterVoltageP1($id);
- case 'get_counter_voltage_p2':
- return $this->getCounterVoltageP2($id);
- case 'get_counter_voltage_p3':
- return $this->getCounterVoltageP3($id);
- case 'get_counter_voltages':
- return $this->getCounterVoltages($id);
- case 'get_counter_current_p1':
- return $this->getCounterCurrentP1($id);
- case 'get_counter_current_p2':
- return $this->getCounterCurrentP2($id);
- case 'get_counter_current_p3':
- return $this->getCounterCurrentP3($id);
- case 'get_counter_currents':
- return $this->getCounterCurrents($id);
- case 'get_counter_power':
- return $this->getCounterPower($id);
- case 'get_counter_powers':
- return $this->getCounterPowers($id);
- case 'get_counter_power_factors':
- return $this->getCounterPowerFactors($id);
- case 'get_counter_imported':
- return $this->getCounterImported($id);
- case 'get_counter_exported':
- return $this->getCounterExported($id);
- case 'get_counter_daily_imported':
- return $this->getCounterDailyImported($id);
- case 'get_counter_daily_exported':
- return $this->getCounterDailyExported($id);
- case 'get_counter_frequency':
- return $this->getCounterFrequency($id);
- case 'get_counter_fault_str':
- return $this->getCounterFaultStr($id);
- case 'get_counter_fault_state':
- return $this->getCounterFaultState($id);
-
- // Battery - Zusätzliche Einzelwerte
- case 'get_battery':
- return $this->getBattery($id);
- case 'get_battery_power':
- return $this->getBatteryPower($id);
- case 'get_battery_soc':
- return $this->getBatterySoc($id);
- case 'get_battery_currents':
- return $this->getBatteryCurrents($id);
- case 'get_battery_imported':
- return $this->getBatteryImported($id);
- case 'get_battery_exported':
- return $this->getBatteryExported($id);
- case 'get_battery_daily_imported':
- return $this->getBatteryDailyImported($id);
- case 'get_battery_daily_exported':
- return $this->getBatteryDailyExported($id);
- case 'get_battery_fault_str':
- return $this->getBatteryFaultStr($id);
- case 'get_battery_fault_state':
- return $this->getBatteryFaultState($id);
- case 'get_battery_power_limit_controllable':
- return $this->getBatteryPowerLimitControllable($id);
-
- // PV - Zusätzliche Einzelwerte
- case 'get_pv':
- return $this->getPv($id);
- case 'get_pv_power':
- return $this->getPvPower($id);
- case 'get_pv_currents':
- return $this->getPvCurrents($id);
- case 'get_pv_imported':
- return $this->getPvImported($id);
- case 'get_pv_exported':
- return $this->getPvExported($id);
- case 'get_pv_daily_exported':
- return $this->getPvDailyExported($id);
- case 'get_pv_monthly_exported':
- return $this->getPvMonthlyExported($id);
- case 'get_pv_yearly_exported':
- return $this->getPvYearlyExported($id);
- case 'get_pv_fault_str':
- return $this->getPvFaultStr($id);
- case 'get_pv_fault_state':
- return $this->getPvFaultState($id);
-
- // Consumer - Alle Daten
- case 'consumer':
- case 'get_consumer':
- case 'get_consumer_all':
- return $this->getConsumerAll($id);
- // Consumer - Einzelwerte
- case 'get_consumer_power':
- return $this->getConsumerPower($id);
- case 'get_consumer_currents':
- return $this->getConsumerCurrents($id);
- case 'get_consumer_voltages':
- return $this->getConsumerVoltages($id);
- case 'get_consumer_powers':
- return $this->getConsumerPowers($id);
- case 'get_consumer_imported':
- return $this->getConsumerImported($id);
- case 'get_consumer_exported':
- return $this->getConsumerExported($id);
- case 'get_consumer_daily_imported':
- return $this->getConsumerDailyImported($id);
- case 'get_consumer_phases_in_use':
- return $this->getConsumerPhasesInUse($id);
- case 'get_consumer_charge_state':
- return $this->getConsumerChargeState($id);
- case 'get_consumer_state_str':
- return $this->getConsumerStateStr($id);
- case 'get_consumer_fault_str':
- return $this->getConsumerFaultStr($id);
- case 'get_consumer_fault_state':
- return $this->getConsumerFaultState($id);
- case 'get_consumer_usage_type':
- return $this->getConsumerUsageType($id);
-
- case 'get_io_output_all':
- return $this->getIoOutputAll($id);
- case 'get_io_output':
- return $this->getIoOutput($id, $options['io_output'] ?? null, $options['io_output_type'] ?? null);
-
- case 'get_lastlivevaluesjson':
- return $this->getLastLiveValuesJson();
-
- default:
- return null;
- }
- }
-
- /**
- * Parameter schreiben
- */
- public function writeParameter($param, $value, $targetId = null, $options = [])
- {
- try {
- switch ($param) {
- case 'set_chargemode':
- return $this->setChargemode($targetId, $value);
- case 'chargecurrent':
- return $this->setChargecurrent($targetId, $value);
- case 'minimal_permanent_current':
- return $this->setMinimalPermanentCurrent($targetId, $value);
- case 'minimal_pv_soc':
- return $this->setMinimalPvSoc($targetId, $value);
- case 'max_price_eco':
- return $this->setMaxPriceEco($targetId, $value);
- case 'chargepoint_lock':
- return $this->setChargepointLock($targetId, $value);
- case 'bat_mode':
- return $this->setBatMode($value);
- case 'bat_power_reserve':
- return $this->setBatPowerReserve($value);
- case 'instant_charging_limit':
- return $this->setInstantChargingLimit($targetId, $value);
- case 'instant_charging_amount':
- return $this->setInstantChargingAmount($targetId, $value);
- case 'instant_charging_soc':
- return $this->setInstantChargingSoc($targetId, $value);
- case 'pv_charging_limit':
- return $this->setPvChargingLimit($targetId, $value);
- case 'pv_charging_amount':
- return $this->setPvChargingAmount($targetId, $value);
- case 'pv_charging_soc':
- return $this->setPvChargingSoc($targetId, $value);
- case 'vehicle':
- return $this->setVehicle($targetId, $value);
- case 'manual_soc':
- return $this->setManualSoc($targetId, $value);
- case 'set_io_output':
- return $this->setIoOutput($targetId, $value, $options['io_output'] ?? null, $options['io_output_type'] ?? null);
- default:
- return ['success' => false, 'message' => 'Unknown write parameter'];
- }
- } catch (Exception $e) {
- return ['success' => false, 'message' => $e->getMessage()];
- }
- }
-
- /**
- * Alle Daten eines Ladepunkts (Performance-optimiert)
- */
- private function getChargepointAll($id)
- {
- $prefix = "openWB/chargepoint/{$id}/get/";
-
- // Alle benötigten Topics in einem Aufruf abfragen
- $topics = [
- $prefix . 'power',
- $prefix . 'voltages',
- $prefix . 'currents',
- $prefix . 'powers',
- $prefix . 'state_str',
- $prefix . 'fault_str',
- $prefix . 'fault_state',
- $prefix . 'imported',
- $prefix . 'exported',
- $prefix . 'daily_imported',
- $prefix . 'daily_exported',
- $prefix . 'phases_in_use',
- $prefix . 'plug_state',
- $prefix . 'charge_state',
- $prefix . 'soc',
- $prefix . 'soc_timestamp',
- $prefix . 'vehicle_id',
- $prefix . 'evse_current',
- $prefix . 'frequency',
- $prefix . 'power_factors',
- $prefix . 'rfid',
- $prefix . 'rfid_timestamp',
- $prefix . 'connected_vehicle/info',
- "openWB/chargepoint/{$id}/config",
-
- "openWB/chargepoint/{$id}/set/charge_template"
- ];
-
- $values = $this->mqttClient->getMultipleValues($topics);
-
- // Arrays parsen
- try {
- $voltages = json_decode($values[$prefix . 'voltages'] ?? '[]', true) ?: [0, 0, 0];
- $currents = json_decode($values[$prefix . 'currents'] ?? '[]', true) ?: [0, 0, 0];
- $powers = json_decode($values[$prefix . 'powers'] ?? '[]', true) ?: [0, 0, 0];
- $powerFactors = json_decode($values[$prefix . 'power_factors'] ?? '[]', true) ?: [0, 0, 0];
- } catch (Exception $e) {
- $voltages = [0, 0, 0];
- $currents = [0, 0, 0];
- $powers = [0, 0, 0];
- $powerFactors = [0, 0, 0];
- }
-
- // Chargemode und min_current aus Template extrahieren
- $chargemode = 'stop';
- $minCurrent = 0;
- $instantChargingCurrent = 0;
- $pvChargingMinCurrent = 0;
- $chargeTemplateName = null;
- $connectedVehicleName = null;
- $configName = null;
- $instantChargingLimit = 'none';
- $instantChargingAmount = 0;
- $instantChargingSoc = 0;
- $pvChargingLimit = 'none';
- $pvChargingAmount = 0;
- $pvChargingSoc = 0;
- $maxPriceEco = 0;
- $soc = 0;
- $rangeCharged = 0;
-
- try {
- $template = json_decode($values["openWB/chargepoint/{$id}/set/charge_template"] ?? '{}', true);
- $chargemode = $template['chargemode']['selected'] ?? 'stop';
- $chargeTemplateName = $template['name'] ?? null;
-
- // Alle relevanten Ströme aus dem Template extrahieren
- $instantChargingCurrent = $template['chargemode']['instant_charging']['current'] ?? 0;
- $pvChargingMinCurrent = $template['chargemode']['pv_charging']['min_current'] ?? 0;
-
- // Instant Charging Limit-Parameter extrahieren
- $instantChargingLimit = $template['chargemode']['instant_charging']['limit']['selected'] ?? 'none';
- $instantChargingAmount = $template['chargemode']['instant_charging']['limit']['amount'] ?? 0;
- $instantChargingSoc = $template['chargemode']['instant_charging']['limit']['soc'] ?? 0;
-
- // PV Charging Limit-Parameter extrahieren
- $pvChargingLimit = $template['chargemode']['pv_charging']['limit']['selected'] ?? 'none';
- $pvChargingAmount = $template['chargemode']['pv_charging']['limit']['amount'] ?? 0;
- $pvChargingSoc = $template['chargemode']['pv_charging']['limit']['soc'] ?? 0;
-
- // ECO Charging max_price extrahieren
- $maxPriceEco = isset($template['chargemode']['eco_charging']['max_price']) ? number_format((float)$template['chargemode']['eco_charging']['max_price'], 6, '.', '') : '0.0000';
- $maxPriceEco = $maxPriceEco * 100000;
-
- // SoC und Range aus connected_vehicle extrahieren
- $connectedVehicleSocTopic = "openWB/chargepoint/{$id}/get/connected_vehicle/soc";
- $connectedVehicleSocValue = $this->mqttClient->getValue($connectedVehicleSocTopic);
- $connectedVehicleSocData = json_decode($connectedVehicleSocValue ?? '{}', true);
- $soc = $connectedVehicleSocData['soc'] ?? 0;
- $rangeCharged = $connectedVehicleSocData['range_charged'] ?? 0;
-
- // min_current aus dem entsprechenden Lademodus extrahieren
- switch ($chargemode) {
- case 'instant_charging':
- $minCurrent = $instantChargingCurrent;
- break;
- case 'pv_charging':
- case 'eco_charging':
- $minCurrent = $pvChargingMinCurrent;
- break;
- case 'scheduled_charging':
- $minCurrent = $template['chargemode']['scheduled_charging']['current'] ?? 0;
- break;
- }
- } catch (Exception $e) {
- // Fallback
- }
-
- // Connected Vehicle Name aus JSON extrahieren
- try {
- $connectedVehicleInfo = json_decode($values[$prefix . 'connected_vehicle/info'] ?? '{}', true);
- $connectedVehicleName = $connectedVehicleInfo['name'] ?? null;
- } catch (Exception $e) {
- // Fallback
- }
-
- // Config Name aus JSON extrahieren
- try {
- $configInfo = json_decode($values["openWB/chargepoint/{$id}/config"] ?? '{}', true);
- $configName = $configInfo['name'] ?? null;
- } catch (Exception $e) {
- // Fallback
- }
-
- $data = [
- "chargepoint_{$id}" => [
- 'power' => floatval($values[$prefix . 'power'] ?? 0),
- 'voltages' => [
- floatval($voltages[0] ?? 0),
- floatval($voltages[1] ?? 0),
- floatval($voltages[2] ?? 0)
- ],
- 'currents' => [
- floatval($currents[0] ?? 0),
- floatval($currents[1] ?? 0),
- floatval($currents[2] ?? 0)
- ],
- 'powers' => [
- floatval($powers[0] ?? 0),
- floatval($powers[1] ?? 0),
- floatval($powers[2] ?? 0)
- ],
- 'state_str' => $this->decodeStringValue($values[$prefix . 'state_str'] ?? null, 'Unbekannt'),
- 'fault_str' => $this->decodeStringValue($values[$prefix . 'fault_str'] ?? null, 'Kein Fehler'),
- 'fault_state' => intval($values[$prefix . 'fault_state'] ?? 0),
- 'imported' => floatval($values[$prefix . 'imported'] ?? 0),
- 'exported' => floatval($values[$prefix . 'exported'] ?? 0),
- 'daily_imported' => floatval($values[$prefix . 'daily_imported'] ?? 0),
- 'daily_exported' => floatval($values[$prefix . 'daily_exported'] ?? 0),
- 'phases_in_use' => intval($values[$prefix . 'phases_in_use'] ?? 1),
- 'plug_state' => $this->parseBooleanValue($values[$prefix . 'plug_state'] ?? 'false'),
- 'charge_state' => $this->parseBooleanValue($values[$prefix . 'charge_state'] ?? 'false'),
- 'pro_soc' => floatval($values[$prefix . 'soc'] ?? 0),
- 'soc_timestamp' => $values[$prefix . 'soc_timestamp'] ?? null,
- 'vehicle_id' => $values[$prefix . 'vehicle_id'] ?? null,
- 'evse_current' => floatval($values[$prefix . 'evse_current'] ?? 0),
- 'frequency' => floatval($values[$prefix . 'frequency'] ?? 0),
- 'power_factors' => [
- floatval($powerFactors[0] ?? 0),
- floatval($powerFactors[1] ?? 0),
- floatval($powerFactors[2] ?? 0)
- ],
- 'rfid' => $this->decodeStringValue($values[$prefix . 'rfid'] ?? null, null),
- 'rfid_timestamp' => $values[$prefix . 'rfid_timestamp'] ?? null,
- 'config_name' => $configName,
- 'connected_vehicle_name' => $connectedVehicleName,
- 'charge_template_name' => $chargeTemplateName,
- 'min_current' => floatval($minCurrent),
- 'instant_charging_current' => floatval($instantChargingCurrent),
- 'pv_charging_min_current' => floatval($pvChargingMinCurrent),
- 'instant_charging_limit' => $instantChargingLimit,
- 'instant_charging_amount' => intval($instantChargingAmount),
- 'instant_charging_soc' => intval($instantChargingSoc),
- 'pv_charging_limit' => $pvChargingLimit,
- 'pv_charging_amount' => intval($pvChargingAmount),
- 'pv_charging_soc' => intval($pvChargingSoc),
- 'max_price_eco' => floatval($maxPriceEco),
- 'soc' => floatval($soc),
- 'range_charged' => floatval($rangeCharged),
- 'chargemode' => $chargemode
- ]
- ];
-
- // manual_lock Status auslesen
- $manualLockTopic = "openWB/chargepoint/{$id}/set/manual_lock";
- try {
- $manualLock = $this->mqttClient->getValue($manualLockTopic);
- } catch (Exception $e) {
- $manualLock = 'false';
- }
- $data["chargepoint_{$id}"]['manual_lock'] = $this->parseBooleanValue($manualLock ?? 'false');
-
- return $data;
- }
-
- /**
- * Boolean-Wert parsen
- */
- private function parseBooleanValue($value)
- {
- if (is_bool($value)) {
- return $value;
- }
-
- $value = strtolower(trim($value, '"'));
- return in_array($value, ['true', '1', 'yes', 'on']);
- }
-
- /**
- * String-Wert aus MQTT dekodieren. OpenWB kodiert auch reine String-Payloads per
- * json.dumps() (z.B. "Kein Fehler." statt Kein Fehler.) - ohne json_decode() blieben die
- * Anführungszeichen im Wert erhalten und würden beim äußeren json_encode() der Antwort
- * nochmal escaped. Fällt bei nicht-JSON-Werten (z.B. leer) auf den Rohwert/Default zurück.
- */
- private function decodeStringValue($value, $default = '')
- {
- if ($value === null || $value === '') {
- return $default;
- }
- $decoded = json_decode($value, true);
- return is_string($decoded) ? $decoded : $value;
- }
-
- /**
- * Spannung einer Phase
- */
- private function getChargepointVoltage($id, $phase)
- {
- // OpenWB gibt Spannungen als Array zurück: [237.79, 0, 0]
- $topic = "openWB/chargepoint/{$id}/get/voltages";
- $voltagesJson = $this->mqttClient->getValue($topic);
-
- try {
- $voltages = json_decode($voltagesJson, true);
- $voltage = $voltages[$phase - 1] ?? 0; // Array ist 0-basiert, Phase 1-basiert
-
- return [
- "chargepoint_{$id}" => [
- "voltage_p{$phase}" => floatval($voltage)
- ]
- ];
- } catch (Exception $e) {
- return [
- "chargepoint_{$id}" => [
- "voltage_p{$phase}" => 0
- ]
- ];
- }
- }
-
- /**
- * Alle Spannungen
- */
- private function getChargepointVoltages($id)
- {
- $topic = "openWB/chargepoint/{$id}/get/voltages";
- $voltagesJson = $this->mqttClient->getValue($topic);
-
- try {
- $voltages = json_decode($voltagesJson, true);
-
- return [
- "chargepoint_{$id}" => [
- 'voltages' => [
- floatval($voltages[0] ?? 0),
- floatval($voltages[1] ?? 0),
- floatval($voltages[2] ?? 0)
- ]
- ]
- ];
- } catch (Exception $e) {
- return [
- "chargepoint_{$id}" => [
- 'voltages' => [0, 0, 0]
- ]
- ];
- }
- }
-
- /**
- * Strom einer Phase
- */
- private function getChargepointCurrent($id, $phase)
- {
- // OpenWB gibt Ströme als Array zurück: [0, 0, 0]
- $topic = "openWB/chargepoint/{$id}/get/currents";
- $currentsJson = $this->mqttClient->getValue($topic);
-
- try {
- $currents = json_decode($currentsJson, true);
- $current = $currents[$phase - 1] ?? 0; // Array ist 0-basiert, Phase 1-basiert
-
- return [
- "chargepoint_{$id}" => [
- "current_p{$phase}" => floatval($current)
- ]
- ];
- } catch (Exception $e) {
- return [
- "chargepoint_{$id}" => [
- "current_p{$phase}" => 0
- ]
- ];
- }
- }
-
- /**
- * Alle Ströme
- */
- private function getChargepointCurrents($id)
- {
- $topic = "openWB/chargepoint/{$id}/get/currents";
- $currentsJson = $this->mqttClient->getValue($topic);
-
- try {
- $currents = json_decode($currentsJson, true);
-
- return [
- "chargepoint_{$id}" => [
- 'currents' => [
- floatval($currents[0] ?? 0),
- floatval($currents[1] ?? 0),
- floatval($currents[2] ?? 0)
- ]
- ]
- ];
- } catch (Exception $e) {
- return [
- "chargepoint_{$id}" => [
- 'currents' => [0, 0, 0]
- ]
- ];
- }
- }
-
- /**
- * Gesamtleistung
- */
- private function getChargepointPower($id)
- {
- if ($this->isTotalId($id)) {
- $sum = $this->getTotalMetricValue('chargepoint', 'power');
- return ['chargepoint_total' => ['power' => $sum]];
- }
-
- try {
- $topic = "openWB/chargepoint/{$id}/get/power";
- $value = $this->mqttClient->getValue($topic);
- return ["chargepoint_{$id}" => ['power' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['power' => 0]];
- }
- }
-
- /**
- * Leistung aller Phasen
- */
- private function getChargepointPowers($id)
- {
- $topic = "openWB/chargepoint/{$id}/get/powers";
- $powersJson = $this->mqttClient->getValue($topic);
-
- try {
- $powers = json_decode($powersJson, true);
-
- return [
- "chargepoint_{$id}" => [
- 'powers' => [
- floatval($powers[0] ?? 0),
- floatval($powers[1] ?? 0),
- floatval($powers[2] ?? 0)
- ]
- ]
- ];
- } catch (Exception $e) {
- return [
- "chargepoint_{$id}" => [
- 'powers' => [0, 0, 0]
- ]
- ];
- }
- }
-
- /**
- * Batterie-Daten (Performance-optimiert)
- */
- private function getBattery($id)
- {
- $prefix = "openWB/bat/{$id}/get/";
-
- // Alle benötigten Topics in einem Aufruf abfragen
- $topics = [
- $prefix . 'power',
- $prefix . 'soc',
- $prefix . 'currents',
- $prefix . 'imported',
- $prefix . 'exported',
- $prefix . 'daily_imported',
- $prefix . 'daily_exported',
- $prefix . 'fault_str',
- $prefix . 'fault_state',
- $prefix . 'power_limit_controllable'
- ];
-
- $values = $this->mqttClient->getMultipleValues($topics);
-
- // Currents Array parsen
- try {
- $currents = json_decode($values[$prefix . 'currents'] ?? '[]', true) ?: [0, 0, 0];
- } catch (Exception $e) {
- $currents = [0, 0, 0];
- }
-
- return [
- "battery_{$id}" => [
- 'power' => floatval($values[$prefix . 'power'] ?? 0),
- 'soc' => intval($values[$prefix . 'soc'] ?? 0),
- 'currents' => [
- floatval($currents[0] ?? 0),
- floatval($currents[1] ?? 0),
- floatval($currents[2] ?? 0)
- ],
- 'imported' => floatval($values[$prefix . 'imported'] ?? 0),
- 'exported' => floatval($values[$prefix . 'exported'] ?? 0),
- 'daily_imported' => floatval($values[$prefix . 'daily_imported'] ?? 0),
- 'daily_exported' => floatval($values[$prefix . 'daily_exported'] ?? 0),
- 'fault_str' => $this->decodeStringValue($values[$prefix . 'fault_str'] ?? null, 'Kein Fehler'),
- 'fault_state' => intval($values[$prefix . 'fault_state'] ?? 0),
- 'power_limit_controllable' => $this->parseBooleanValue($values[$prefix . 'power_limit_controllable'] ?? 'false')
- ]
- ];
- }
-
- /**
- * PV-Daten (Performance-optimiert)
- */
- private function getPv($id)
- {
- $prefix = "openWB/pv/{$id}/get/";
-
- // Alle benötigten Topics in einem Aufruf abfragen
- $topics = [
- $prefix . 'power',
- $prefix . 'currents',
- $prefix . 'exported',
- $prefix . 'daily_exported',
- $prefix . 'monthly_exported',
- $prefix . 'yearly_exported',
- $prefix . 'fault_str',
- $prefix . 'fault_state'
- ];
-
- $values = $this->mqttClient->getMultipleValues($topics);
-
- // Currents Array parsen
- try {
- $currents = json_decode($values[$prefix . 'currents'] ?? '[]', true) ?: [0, 0, 0];
- } catch (Exception $e) {
- $currents = [0, 0, 0];
- }
-
- return [
- "pv_{$id}" => [
- 'power' => floatval($values[$prefix . 'power'] ?? 0),
- 'currents' => [
- floatval($currents[0] ?? 0),
- floatval($currents[1] ?? 0),
- floatval($currents[2] ?? 0)
- ],
- 'exported' => floatval($values[$prefix . 'exported'] ?? 0),
- 'daily_exported' => floatval($values[$prefix . 'daily_exported'] ?? 0),
- 'monthly_exported' => floatval($values[$prefix . 'monthly_exported'] ?? 0),
- 'yearly_exported' => floatval($values[$prefix . 'yearly_exported'] ?? 0),
- 'fault_str' => $this->decodeStringValue($values[$prefix . 'fault_str'] ?? null, 'Kein Fehler'),
- 'fault_state' => intval($values[$prefix . 'fault_state'] ?? 0)
- ]
- ];
- }
-
- /**
- * Verbraucher-Daten (Consumer) - Performance-optimiert
- */
- private function getConsumerAll($id)
- {
- $prefix = "openWB/consumer/{$id}/get/";
-
- // Alle benötigten Topics in einem Aufruf abfragen
- $topics = [
- $prefix . 'power',
- $prefix . 'currents',
- $prefix . 'voltages',
- $prefix . 'powers',
- $prefix . 'imported',
- $prefix . 'exported',
- $prefix . 'daily_imported',
- $prefix . 'phases_in_use',
- $prefix . 'charge_state',
- $prefix . 'state_str',
- $prefix . 'fault_str',
- $prefix . 'fault_state',
- "openWB/consumer/{$id}/usage"
- ];
-
- $values = $this->mqttClient->getMultipleValues($topics);
-
- // Arrays parsen
- try {
- $currents = json_decode($values[$prefix . 'currents'] ?? '[]', true) ?: [0, 0, 0];
- $voltages = json_decode($values[$prefix . 'voltages'] ?? '[]', true) ?: [0, 0, 0];
- $powers = json_decode($values[$prefix . 'powers'] ?? '[]', true) ?: [0, 0, 0];
- } catch (Exception $e) {
- $currents = [0, 0, 0];
- $voltages = [0, 0, 0];
- $powers = [0, 0, 0];
- }
-
- $usageType = null;
- try {
- $usageInfo = json_decode($values["openWB/consumer/{$id}/usage"] ?? '{}', true);
- $usageType = $usageInfo['type'] ?? null;
- } catch (Exception $e) {
- // Fallback
- }
-
- return [
- "consumer_{$id}" => [
- 'usage_type' => $usageType,
- 'power' => floatval($values[$prefix . 'power'] ?? 0),
- 'currents' => [
- floatval($currents[0] ?? 0),
- floatval($currents[1] ?? 0),
- floatval($currents[2] ?? 0)
- ],
- 'voltages' => [
- floatval($voltages[0] ?? 0),
- floatval($voltages[1] ?? 0),
- floatval($voltages[2] ?? 0)
- ],
- 'powers' => [
- floatval($powers[0] ?? 0),
- floatval($powers[1] ?? 0),
- floatval($powers[2] ?? 0)
- ],
- 'imported' => floatval($values[$prefix . 'imported'] ?? 0),
- 'exported' => floatval($values[$prefix . 'exported'] ?? 0),
- 'daily_imported' => floatval($values[$prefix . 'daily_imported'] ?? 0),
- 'phases_in_use' => intval($values[$prefix . 'phases_in_use'] ?? 0),
- 'charge_state' => $this->parseBooleanValue($values[$prefix . 'charge_state'] ?? 'false'),
- 'state_str' => $this->decodeStringValue($values[$prefix . 'state_str'] ?? null, null),
- 'fault_str' => $this->decodeStringValue($values[$prefix . 'fault_str'] ?? null, 'Kein Fehler'),
- 'fault_state' => intval($values[$prefix . 'fault_state'] ?? 0)
- ]
- ];
- }
-
- /**
- * Zähler-Daten (Counter) - Performance-optimiert
- */
- private function getCounter($id)
- {
- $prefix = "openWB/counter/{$id}/get/";
-
- // Alle benötigten Topics in einem Aufruf abfragen
- $topics = [
- $prefix . 'power',
- $prefix . 'voltages',
- $prefix . 'currents',
- $prefix . 'powers',
- $prefix . 'power_factors',
- $prefix . 'frequency',
- $prefix . 'exported',
- $prefix . 'daily_exported',
- $prefix . 'imported',
- $prefix . 'daily_imported',
- $prefix . 'fault_str',
- $prefix . 'fault_state'
- ];
-
- $values = $this->mqttClient->getMultipleValues($topics);
-
- // Arrays parsen
- try {
- $voltages = json_decode($values[$prefix . 'voltages'] ?? '[]', true) ?: [0, 0, 0];
- $currents = json_decode($values[$prefix . 'currents'] ?? '[]', true) ?: [0, 0, 0];
- $powers = json_decode($values[$prefix . 'powers'] ?? '[]', true) ?: [0, 0, 0];
- $power_factors = json_decode($values[$prefix . 'power_factors'] ?? '[]', true) ?: [0, 0, 0];
- } catch (Exception $e) {
- $voltages = [0, 0, 0];
- $currents = [0, 0, 0];
- $powers = [0, 0, 0];
- $power_factors = [0, 0, 0];
- }
-
- return [
- "counter_{$id}" => [
- 'power' => floatval($values[$prefix . 'power'] ?? 0),
- 'voltages' => [
- floatval($voltages[0] ?? 0),
- floatval($voltages[1] ?? 0),
- floatval($voltages[2] ?? 0)
- ],
- 'currents' => [
- floatval($currents[0] ?? 0),
- floatval($currents[1] ?? 0),
- floatval($currents[2] ?? 0)
- ],
- 'powers' => [
- floatval($powers[0] ?? 0),
- floatval($powers[1] ?? 0),
- floatval($powers[2] ?? 0)
- ],
- 'power_factors' => [
- floatval($power_factors[0] ?? 0),
- floatval($power_factors[1] ?? 0),
- floatval($power_factors[2] ?? 0)
- ],
- 'frequency' => floatval($values[$prefix . 'frequency'] ?? 50.0),
- 'exported' => floatval($values[$prefix . 'exported'] ?? 0),
- 'daily_exported' => floatval($values[$prefix . 'daily_exported'] ?? 0),
- 'imported' => floatval($values[$prefix . 'imported'] ?? 0),
- 'daily_imported' => floatval($values[$prefix . 'daily_imported'] ?? 0),
- 'fault_str' => $this->decodeStringValue($values[$prefix . 'fault_str'] ?? null, 'Kein Fehler'),
- 'fault_state' => intval($values[$prefix . 'fault_state'] ?? 0)
- ]
- ];
- }
-
- /**
- * Lademodus setzen (OpenWB Template-System)
- */
- private function setChargemode($chargepointId, $mode)
- {
- // Gültige Modi mapping (sowohl kurze als auch vollständige Namen)
- $validModes = [
- 'instant' => 'instant_charging',
- 'pv' => 'pv_charging',
- 'eco' => 'eco_charging',
- 'stop' => 'stop',
- 'target' => 'scheduled_charging',
- // Vollständige Namen direkt unterstützen
- 'instant_charging' => 'instant_charging',
- 'pv_charging' => 'pv_charging',
- 'eco_charging' => 'eco_charging',
- 'scheduled_charging' => 'scheduled_charging'
- ];
-
- if (!isset($validModes[$mode])) {
- return ['success' => false, 'message' => 'Invalid chargemode. Valid modes: ' . implode(', ', array_keys($validModes))];
- }
-
- $selectedMode = $validModes[$mode];
-
- try {
- // 1. Aktuelles Template von /set/charge_template auslesen
- $templateTopic = "openWB/chargepoint/{$chargepointId}/set/charge_template";
- $templateJson = $this->mqttClient->getValue($templateTopic);
-
- if (!$templateJson) {
- return ['success' => false, 'message' => "Could not read current charge template from topic: {$templateTopic}"];
- }
-
- $template = json_decode($templateJson, true);
- if ($template === null) {
- return ['success' => false, 'message' => 'Invalid charge template format - JSON decode failed: ' . json_last_error_msg() . ". Template JSON: " . substr($templateJson, 0, 200)];
- }
-
- if (!is_array($template)) {
- return ['success' => false, 'message' => 'Invalid charge template format - not an array'];
- }
-
- // 2. Chargemode im Template ändern
- if (!isset($template['chargemode'])) {
- $template['chargemode'] = [];
- }
-
- $template['chargemode']['selected'] = $selectedMode;
-
- // 3. Geändertes Template an /set/charge_template zurückschreiben
- $setTopic = "openWB/set/chargepoint/{$chargepointId}/set/charge_template";
- $newTemplateJson = json_encode($template);
-
- if ($this->mqttClient->setValue($setTopic, $newTemplateJson)) {
- return ['success' => true, 'message' => "Chargemode set to {$mode} ({$selectedMode})"];
- }
-
- return ['success' => false, 'message' => 'Failed to update charge template'];
- } catch (Exception $e) {
- return ['success' => false, 'message' => 'Error setting chargemode: ' . $e->getMessage()];
- }
- }
-
- /**
- * Ladestrom setzen (instant_charging.current im Template)
- */
- private function setChargecurrent($chargepointId, $current)
- {
- try {
- $templateTopic = "openWB/chargepoint/{$chargepointId}/set/charge_template";
- $templateJson = $this->mqttClient->getValue($templateTopic);
- if (!$templateJson) {
- return ['success' => false, 'message' => 'Could not read current charge template from set topic'];
- }
- $template = json_decode($templateJson, true);
- if (!$template || !isset($template['chargemode']['instant_charging'])) {
- return ['success' => false, 'message' => 'Invalid charge template format or missing instant_charging'];
- }
- $template['chargemode']['instant_charging']['current'] = floatval($current);
- $setTopic = "openWB/set/chargepoint/{$chargepointId}/set/charge_template";
- $newTemplateJson = json_encode($template);
- if ($this->mqttClient->setValue($setTopic, $newTemplateJson)) {
- return ['success' => true, 'message' => "Chargecurrent set to {$current}A for chargepoint {$chargepointId}"];
- }
- return ['success' => false, 'message' => 'Failed to update charge template'];
- } catch (Exception $e) {
- return ['success' => false, 'message' => 'Error setting chargecurrent: ' . $e->getMessage()];
- }
- }
-
- /**
- * Minimalen permanenten Strom setzen (pv_charging.min_current im Template)
- */
- private function setMinimalPermanentCurrent($chargepointId, $value)
- {
- try {
- $templateTopic = "openWB/chargepoint/{$chargepointId}/set/charge_template";
- $templateJson = $this->mqttClient->getValue($templateTopic);
- if (!$templateJson) {
- return ['success' => false, 'message' => 'Could not read current charge template from set topic'];
- }
- $template = json_decode($templateJson, true);
- if (!$template || !isset($template['chargemode']['pv_charging'])) {
- return ['success' => false, 'message' => 'Invalid charge template format or missing pv_charging'];
- }
- $template['chargemode']['pv_charging']['min_current'] = floatval($value);
- $setTopic = "openWB/set/chargepoint/{$chargepointId}/set/charge_template";
- $newTemplateJson = json_encode($template);
- if ($this->mqttClient->setValue($setTopic, $newTemplateJson)) {
- return ['success' => true, 'message' => "Minimal permanent current set to {$value}A for chargepoint {$chargepointId}"];
- }
- return ['success' => false, 'message' => 'Failed to update charge template'];
- } catch (Exception $e) {
- return ['success' => false, 'message' => 'Error setting minimal permanent current: ' . $e->getMessage()];
- }
- }
-
- /**
- * Minimalen PV SoC setzen (pv_charging.min_soc im Template)
- */
- private function setMinimalPvSoc($chargepointId, $value)
- {
- try {
- $templateTopic = "openWB/chargepoint/{$chargepointId}/set/charge_template";
- $templateJson = $this->mqttClient->getValue($templateTopic);
- if (!$templateJson) {
- return ['success' => false, 'message' => 'Could not read current charge template from set topic'];
- }
- $template = json_decode($templateJson, true);
- if (!$template || !isset($template['chargemode']['pv_charging'])) {
- return ['success' => false, 'message' => 'Invalid charge template format or missing pv_charging'];
- }
- $template['chargemode']['pv_charging']['min_soc'] = intval($value);
- $setTopic = "openWB/set/chargepoint/{$chargepointId}/set/charge_template";
- $newTemplateJson = json_encode($template);
- if ($this->mqttClient->setValue($setTopic, $newTemplateJson)) {
- return ['success' => true, 'message' => "Minimal PV SoC set to {$value}% for chargepoint {$chargepointId}"];
- }
- return ['success' => false, 'message' => 'Failed to update charge template'];
- } catch (Exception $e) {
- return ['success' => false, 'message' => 'Error setting minimal PV SoC: ' . $e->getMessage()];
- }
- }
-
- /**
- * Maximalen ECO-Preis setzen (eco_charging.max_price im Template)
- */
- private function setMaxPriceEco($chargepointId, $value)
- {
- try {
- $value = $value / 100000;
- $templateTopic = "openWB/chargepoint/{$chargepointId}/set/charge_template";
- $templateJson = $this->mqttClient->getValue($templateTopic);
- if (!$templateJson) {
- return ['success' => false, 'message' => 'Could not read current charge template from set topic'];
- }
- $template = json_decode($templateJson, true);
- if (!$template || !isset($template['chargemode']['eco_charging'])) {
- return ['success' => false, 'message' => 'Invalid charge template format or missing eco_charging'];
- }
- $template['chargemode']['eco_charging']['max_price'] = floatval($value);
- $setTopic = "openWB/set/chargepoint/{$chargepointId}/set/charge_template";
- $newTemplateJson = json_encode($template);
- if ($this->mqttClient->setValue($setTopic, $newTemplateJson)) {
- return ['success' => true, 'message' => "Max price eco set to {$value} for chargepoint {$chargepointId}"];
- }
- return ['success' => false, 'message' => 'Failed to update charge template'];
- } catch (Exception $e) {
- return ['success' => false, 'message' => 'Error setting max price eco: ' . $e->getMessage()];
- }
- }
-
- /**
- * Chargepoint Lock setzen
- */
- private function setChargepointLock($chargepointId, $value)
- {
- $topic = "openWB/set/chargepoint/{$chargepointId}/set/manual_lock";
- $lockValue = $value ? 'true' : 'false';
- if ($this->mqttClient->setValue($topic, $lockValue)) {
- return ['success' => true, 'message' => "Chargepoint lock set to {$lockValue} for chargepoint {$chargepointId}"];
- }
- return ['success' => false, 'message' => 'Failed to set chargepoint lock'];
- }
-
- /**
- * Chargepoint Daily Imported
- */
- private function getChargepointDailyImported($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/get/daily_imported";
- $value = $this->mqttClient->getValue($topic);
- return ["chargepoint_{$id}" => ['daily_imported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['daily_imported' => 0]];
- }
- }
-
- /**
- * Chargepoint Daily Exported
- */
- private function getChargepointDailyExported($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/get/daily_exported";
- $value = $this->mqttClient->getValue($topic);
- return ["chargepoint_{$id}" => ['daily_exported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['daily_exported' => 0]];
- }
- }
-
- /**
- * Chargepoint Frequency
- */
- private function getChargepointFrequency($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/get/frequency";
- $value = $this->mqttClient->getValue($topic);
- return ["chargepoint_{$id}" => ['frequency' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['frequency' => 0]];
- }
- }
-
- /**
- * Chargepoint RFID
- */
- private function getChargepointRfid($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/get/rfid";
- $value = $this->mqttClient->getValue($topic);
- return ["chargepoint_{$id}" => ['rfid' => $this->decodeStringValue($value)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['rfid' => '']];
- }
- }
-
- /**
- * Chargepoint RFID Timestamp
- */
- private function getChargepointRfidTimestamp($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/get/rfid_timestamp";
- $value = $this->mqttClient->getValue($topic);
- return ["chargepoint_{$id}" => ['rfid_timestamp' => intval($value ?? 0)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['rfid_timestamp' => 0]];
- }
- }
-
- /**
- * Chargepoint EVSE Current
- */
- private function getChargepointEvseCurrent($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/get/evse_current";
- $value = $this->mqttClient->getValue($topic);
- return ["chargepoint_{$id}" => ['evse_current' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['evse_current' => 0]];
- }
- }
-
- /**
- * Chargepoint Power Factors (alle Phasen)
- */
- private function getChargepointPowerFactors($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/get/power_factors";
- $value = $this->mqttClient->getValue($topic);
- $powerFactors = json_decode($value ?? '[]', true) ?: [0, 0, 0];
-
- return [
- "chargepoint_{$id}" => [
- 'power_factors' => [
- floatval($powerFactors[0] ?? 0),
- floatval($powerFactors[1] ?? 0),
- floatval($powerFactors[2] ?? 0)
- ]
- ]
- ];
- } catch (Exception $e) {
- return [
- "chargepoint_{$id}" => [
- 'power_factors' => [0, 0, 0]
- ]
- ];
- }
- }
-
- /**
- * Chargepoint Power Factor einzelne Phase
- */
- private function getChargepointPowerFactor($id, $phase)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/get/power_factors";
- $value = $this->mqttClient->getValue($topic);
- $powerFactors = json_decode($value ?? '[]', true) ?: [0, 0, 0];
-
- return [
- "chargepoint_{$id}" => [
- "power_factor_p{$phase}" => floatval($powerFactors[$phase - 1] ?? 0)
- ]
- ];
- } catch (Exception $e) {
- return [
- "chargepoint_{$id}" => [
- "power_factor_p{$phase}" => 0
- ]
- ];
- }
- }
-
- /**
- * Chargepoint Config Name
- */
- private function getChargepointConfigName($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/config";
- $value = $this->mqttClient->getValue($topic);
- $config = json_decode($value ?? '{}', true) ?: [];
-
- return ["chargepoint_{$id}" => ['config_name' => strval($config['name'] ?? '')]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['config_name' => '']];
- }
- }
-
- /**
- * Chargepoint Connected Vehicle Name
- */
- private function getChargepointConnectedVehicleName($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/get/connected_vehicle/info";
- $value = $this->mqttClient->getValue($topic);
- $info = json_decode($value ?? '{}', true) ?: [];
-
- return ["chargepoint_{$id}" => ['connected_vehicle_name' => strval($info['name'] ?? '')]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['connected_vehicle_name' => '']];
- }
- }
-
- /**
- * Chargepoint Charge Template Name
- */
- private function getChargepointChargeTemplateName($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/set/charge_template";
- $value = $this->mqttClient->getValue($topic);
- $template = json_decode($value ?? '{}', true) ?: [];
-
- return ["chargepoint_{$id}" => ['charge_template_name' => strval($template['name'] ?? '')]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['charge_template_name' => '']];
- }
- }
-
- /**
- * Chargepoint Charge Template Min Current
- */
- private function getChargepointChargeTemplateMinCurrent($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/set/charge_template";
- $value = $this->mqttClient->getValue($topic);
- $template = json_decode($value ?? '{}', true) ?: [];
-
- $minCurrent = 0;
- if (isset($template['chargemode']['pv_charging']['min_current'])) {
- $minCurrent = $template['chargemode']['pv_charging']['min_current'];
- }
-
- return ["chargepoint_{$id}" => ['charge_template_min_current' => floatval($minCurrent)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['charge_template_min_current' => 0]];
- }
- }
-
- /**
- * Chargepoint Instant Charging Current
- */
- private function getChargepointInstantChargingCurrent($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/set/charge_template";
- $value = $this->mqttClient->getValue($topic);
- $template = json_decode($value ?? '{}', true) ?: [];
-
- $instantCurrent = 0;
- if (isset($template['chargemode']['instant_charging']['current'])) {
- $instantCurrent = $template['chargemode']['instant_charging']['current'];
- }
-
- return ["chargepoint_{$id}" => ['instant_charging_current' => floatval($instantCurrent)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['instant_charging_current' => 0]];
- }
- }
-
- /**
- * Chargepoint PV Charging Min Current
- */
- private function getChargepointPvChargingMinCurrent($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/set/charge_template";
- $value = $this->mqttClient->getValue($topic);
- $template = json_decode($value ?? '{}', true) ?: [];
-
- $pvMinCurrent = 0;
- if (isset($template['chargemode']['pv_charging']['min_current'])) {
- $pvMinCurrent = $template['chargemode']['pv_charging']['min_current'];
- }
-
- return ["chargepoint_{$id}" => ['pv_charging_min_current' => floatval($pvMinCurrent)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['pv_charging_min_current' => 0]];
- }
- }
-
- /**
- * Chargepoint Imported
- */
- private function getChargepointImported($id)
- {
- if ($this->isTotalId($id)) {
- $sum = $this->getTotalMetricValue('chargepoint', 'imported');
- return ['chargepoint_total' => ['imported' => $sum]];
- }
-
- try {
- $topic = "openWB/chargepoint/{$id}/get/imported";
- $value = $this->mqttClient->getValue($topic);
- return ["chargepoint_{$id}" => ['imported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['imported' => 0]];
- }
- }
-
- /**
- * Chargepoint Exported
- */
- private function getChargepointExported($id)
- {
- if ($this->isTotalId($id)) {
- $sum = $this->getTotalMetricValue('chargepoint', 'exported');
- return ['chargepoint_total' => ['exported' => $sum]];
- }
-
- try {
- $topic = "openWB/chargepoint/{$id}/get/exported";
- $value = $this->mqttClient->getValue($topic);
- return ["chargepoint_{$id}" => ['exported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['exported' => 0]];
- }
- }
-
- /**
- * Chargepoint Monthly Exported
- */
- private function getChargepointMonthlyExported($id)
- {
- if ($this->isTotalId($id)) {
- $sum = $this->getTotalMetricValue('chargepoint', 'monthly_exported');
- return ['chargepoint_total' => ['monthly_exported' => $sum]];
- }
-
- try {
- $topic = "openWB/chargepoint/{$id}/get/monthly_exported";
- $value = $this->mqttClient->getValue($topic);
- return ["chargepoint_{$id}" => ['monthly_exported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['monthly_exported' => 0]];
- }
- }
-
- /**
- * Chargepoint Yearly Exported
- */
- private function getChargepointYearlyExported($id)
- {
- if ($this->isTotalId($id)) {
- $sum = $this->getTotalMetricValue('chargepoint', 'yearly_exported');
- return ['chargepoint_total' => ['yearly_exported' => $sum]];
- }
-
- try {
- $topic = "openWB/chargepoint/{$id}/get/yearly_exported";
- $value = $this->mqttClient->getValue($topic);
- return ["chargepoint_{$id}" => ['yearly_exported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['yearly_exported' => 0]];
- }
- }
-
- /**
- * Chargepoint SoC
- */
- private function getChargepointSoc($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/get/soc";
- $value = $this->mqttClient->getValue($topic);
- return ["chargepoint_{$id}" => ['soc' => intval($value ?? 0)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['soc' => 0]];
- }
- }
-
- /**
- * Chargepoint State String
- */
- private function getChargepointStateStr($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/get/state_str";
- $value = $this->mqttClient->getValue($topic);
- return ["chargepoint_{$id}" => ['state_str' => $this->decodeStringValue($value)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['state_str' => '']];
- }
- }
-
- /**
- * Chargepoint Fault String
- */
- private function getChargepointFaultStr($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/get/fault_str";
- $value = $this->mqttClient->getValue($topic);
- return ["chargepoint_{$id}" => ['fault_str' => $this->decodeStringValue($value)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['fault_str' => '']];
- }
- }
-
- /**
- * Chargepoint Fault State
- */
- private function getChargepointFaultState($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/get/fault_state";
- $value = $this->mqttClient->getValue($topic);
- return ["chargepoint_{$id}" => ['fault_state' => intval($value ?? 0)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['fault_state' => 0]];
- }
- }
-
- /**
- * Chargepoint Phases in Use
- */
- private function getChargepointPhasesInUse($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/get/phases_in_use";
- $value = $this->mqttClient->getValue($topic);
- return ["chargepoint_{$id}" => ['phases_in_use' => intval($value ?? 0)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['phases_in_use' => 0]];
- }
- }
-
- /**
- * Chargepoint Plug State
- */
- private function getChargepointPlugState($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/get/plug_state";
- $value = $this->mqttClient->getValue($topic);
- return ["chargepoint_{$id}" => ['plug_state' => boolval($value ?? false)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['plug_state' => false]];
- }
- }
-
- /**
- * Chargepoint Charge State
- */
- private function getChargepointChargeState($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/get/charge_state";
- $value = $this->mqttClient->getValue($topic);
- return ["chargepoint_{$id}" => ['charge_state' => boolval($value ?? false)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['charge_state' => false]];
- }
- }
-
- /**
- * Chargepoint Chargemode
- */
- private function getChargepointChargemode($id)
- {
- try {
- $topic = "openWB/chargepoint/{$id}/get/chargemode";
- $value = $this->mqttClient->getValue($topic);
- return ["chargepoint_{$id}" => ['chargemode' => $this->decodeStringValue($value)]];
- } catch (Exception $e) {
- return ["chargepoint_{$id}" => ['chargemode' => '']];
- }
- }
-
- /**
- * Instant Charging Limit setzen
- */
- private function setInstantChargingLimit($chargepointId, $value)
- {
- $validLimits = ['none', 'amount', 'soc'];
-
- if (!in_array($value, $validLimits)) {
- return ['success' => false, 'message' => 'Invalid instant_charging_limit. Valid values: ' . implode(', ', $validLimits)];
- }
-
- try {
- $templateTopic = "openWB/chargepoint/{$chargepointId}/set/charge_template";
- $templateJson = $this->mqttClient->getValue($templateTopic);
- if (!$templateJson) {
- return ['success' => false, 'message' => 'Could not read current charge template'];
- }
-
- $template = json_decode($templateJson, true);
- if (!$template || !isset($template['chargemode']['instant_charging'])) {
- return ['success' => false, 'message' => 'Invalid charge template format or missing instant_charging'];
- }
-
- $template['chargemode']['instant_charging']['limit']['selected'] = $value;
- $setTopic = "openWB/set/chargepoint/{$chargepointId}/set/charge_template";
- $newTemplateJson = json_encode($template);
-
- if ($this->mqttClient->setValue($setTopic, $newTemplateJson)) {
- return ['success' => true, 'message' => "Instant charging limit set to {$value} for chargepoint {$chargepointId}"];
- }
- return ['success' => false, 'message' => 'Failed to update charge template'];
- } catch (Exception $e) {
- return ['success' => false, 'message' => 'Error setting instant charging limit: ' . $e->getMessage()];
- }
- }
-
- /**
- * Instant Charging Amount setzen (kWh -> Wh)
- */
- private function setInstantChargingAmount($chargepointId, $value)
- {
- $amount = floatval($value);
- if ($amount < 0) {
- return ['success' => false, 'message' => 'Amount must be >= 0'];
- }
-
- // kWh zu Wh konvertieren
- $amountWh = intval($amount * 1000);
-
- try {
- $templateTopic = "openWB/chargepoint/{$chargepointId}/set/charge_template";
- $templateJson = $this->mqttClient->getValue($templateTopic);
- if (!$templateJson) {
- return ['success' => false, 'message' => 'Could not read current charge template'];
- }
-
- $template = json_decode($templateJson, true);
- if (!$template || !isset($template['chargemode']['instant_charging'])) {
- return ['success' => false, 'message' => 'Invalid charge template format or missing instant_charging'];
- }
-
- $template['chargemode']['instant_charging']['limit']['amount'] = $amountWh;
- $setTopic = "openWB/set/chargepoint/{$chargepointId}/set/charge_template";
- $newTemplateJson = json_encode($template);
-
- if ($this->mqttClient->setValue($setTopic, $newTemplateJson)) {
- return ['success' => true, 'message' => "Instant charging amount set to {$value}kWh ({$amountWh}Wh) for chargepoint {$chargepointId}"];
- }
- return ['success' => false, 'message' => 'Failed to update charge template'];
- } catch (Exception $e) {
- return ['success' => false, 'message' => 'Error setting instant charging amount: ' . $e->getMessage()];
- }
- }
-
- /**
- * Instant Charging SoC setzen
- */
- private function setInstantChargingSoc($chargepointId, $value)
- {
- $soc = intval($value);
- if ($soc < 0 || $soc > 100) {
- return ['success' => false, 'message' => 'SoC must be between 0 and 100'];
- }
-
- try {
- $templateTopic = "openWB/chargepoint/{$chargepointId}/set/charge_template";
- $templateJson = $this->mqttClient->getValue($templateTopic);
- if (!$templateJson) {
- return ['success' => false, 'message' => 'Could not read current charge template'];
- }
-
- $template = json_decode($templateJson, true);
- if (!$template || !isset($template['chargemode']['instant_charging'])) {
- return ['success' => false, 'message' => 'Invalid charge template format or missing instant_charging'];
- }
-
- $template['chargemode']['instant_charging']['limit']['soc'] = $soc;
- $setTopic = "openWB/set/chargepoint/{$chargepointId}/set/charge_template";
- $newTemplateJson = json_encode($template);
-
- if ($this->mqttClient->setValue($setTopic, $newTemplateJson)) {
- return ['success' => true, 'message' => "Instant charging SoC set to {$soc}% for chargepoint {$chargepointId}"];
- }
- return ['success' => false, 'message' => 'Failed to update charge template'];
- } catch (Exception $e) {
- return ['success' => false, 'message' => 'Error setting instant charging SoC: ' . $e->getMessage()];
- }
- }
-
- /**
- * PV Charging Limit setzen
- */
- private function setPvChargingLimit($chargepointId, $value)
- {
- $validLimits = ['none', 'amount', 'soc'];
-
- if (!in_array($value, $validLimits)) {
- return ['success' => false, 'message' => 'Invalid pv_charging_limit. Valid values: ' . implode(', ', $validLimits)];
- }
-
- try {
- $templateTopic = "openWB/chargepoint/{$chargepointId}/set/charge_template";
- $templateJson = $this->mqttClient->getValue($templateTopic);
- if (!$templateJson) {
- return ['success' => false, 'message' => 'Could not read current charge template'];
- }
-
- $template = json_decode($templateJson, true);
- if (!$template || !isset($template['chargemode']['pv_charging'])) {
- return ['success' => false, 'message' => 'Invalid charge template format or missing pv_charging'];
- }
-
- $template['chargemode']['pv_charging']['limit']['selected'] = $value;
- $setTopic = "openWB/set/chargepoint/{$chargepointId}/set/charge_template";
- $newTemplateJson = json_encode($template);
-
- if ($this->mqttClient->setValue($setTopic, $newTemplateJson)) {
- return ['success' => true, 'message' => "PV charging limit set to {$value} for chargepoint {$chargepointId}"];
- }
- return ['success' => false, 'message' => 'Failed to update charge template'];
- } catch (Exception $e) {
- return ['success' => false, 'message' => 'Error setting PV charging limit: ' . $e->getMessage()];
- }
- }
-
- /**
- * PV Charging Amount setzen (kWh -> Wh)
- */
- private function setPvChargingAmount($chargepointId, $value)
- {
- $amount = floatval($value);
- if ($amount < 0) {
- return ['success' => false, 'message' => 'Amount must be >= 0'];
- }
-
- // kWh zu Wh konvertieren
- $amountWh = intval($amount * 1000);
-
- try {
- $templateTopic = "openWB/chargepoint/{$chargepointId}/set/charge_template";
- $templateJson = $this->mqttClient->getValue($templateTopic);
- if (!$templateJson) {
- return ['success' => false, 'message' => 'Could not read current charge template'];
- }
-
- $template = json_decode($templateJson, true);
- if (!$template || !isset($template['chargemode']['pv_charging'])) {
- return ['success' => false, 'message' => 'Invalid charge template format or missing pv_charging'];
- }
-
- $template['chargemode']['pv_charging']['limit']['amount'] = $amountWh;
- $setTopic = "openWB/set/chargepoint/{$chargepointId}/set/charge_template";
- $newTemplateJson = json_encode($template);
-
- if ($this->mqttClient->setValue($setTopic, $newTemplateJson)) {
- return ['success' => true, 'message' => "PV charging amount set to {$value}kWh ({$amountWh}Wh) for chargepoint {$chargepointId}"];
- }
- return ['success' => false, 'message' => 'Failed to update charge template'];
- } catch (Exception $e) {
- return ['success' => false, 'message' => 'Error setting PV charging amount: ' . $e->getMessage()];
- }
- }
-
- /**
- * PV Charging SoC setzen
- */
- private function setPvChargingSoc($chargepointId, $value)
- {
- $soc = intval($value);
- if ($soc < 0 || $soc > 100) {
- return ['success' => false, 'message' => 'SoC must be between 0 and 100'];
- }
-
- try {
- $templateTopic = "openWB/chargepoint/{$chargepointId}/set/charge_template";
- $templateJson = $this->mqttClient->getValue($templateTopic);
- if (!$templateJson) {
- return ['success' => false, 'message' => 'Could not read current charge template'];
- }
-
- $template = json_decode($templateJson, true);
- if (!$template || !isset($template['chargemode']['pv_charging'])) {
- return ['success' => false, 'message' => 'Invalid charge template format or missing pv_charging'];
- }
-
- $template['chargemode']['pv_charging']['limit']['soc'] = $soc;
- $setTopic = "openWB/set/chargepoint/{$chargepointId}/set/charge_template";
- $newTemplateJson = json_encode($template);
-
- if ($this->mqttClient->setValue($setTopic, $newTemplateJson)) {
- return ['success' => true, 'message' => "PV charging SoC set to {$soc}% for chargepoint {$chargepointId}"];
- }
- return ['success' => false, 'message' => 'Failed to update charge template'];
- } catch (Exception $e) {
- return ['success' => false, 'message' => 'Error setting PV charging SoC: ' . $e->getMessage()];
- }
- }
-
- /**
- * Fahrzeug für Chargepoint setzen
- */
- private function setVehicle($chargepointId, $value)
- {
- $vehicleId = intval($value);
- if ($vehicleId < 0) {
- return ['success' => false, 'message' => 'Vehicle ID must be >= 0'];
- }
-
- try {
- $topic = "openWB/set/chargepoint/{$chargepointId}/config/ev";
-
- if ($this->mqttClient->setValue($topic, strval($vehicleId))) {
- return ['success' => true, 'message' => "Vehicle {$vehicleId} assigned to chargepoint {$chargepointId}"];
- }
- return ['success' => false, 'message' => 'Failed to set vehicle'];
- } catch (Exception $e) {
- return ['success' => false, 'message' => 'Error setting vehicle: ' . $e->getMessage()];
- }
- }
-
- /**
- * Manuellen SoC für Fahrzeug setzen
- */
- private function setManualSoc($chargepointId, $value)
- {
- $soc = intval($value);
- if ($soc < 0 || $soc > 100) {
- return ['success' => false, 'message' => 'Manual SoC must be between 0 and 100'];
- }
-
- try {
- // Erst die Fahrzeug-ID vom Chargepoint ermitteln
- $configTopic = "openWB/chargepoint/{$chargepointId}/config";
- $configJson = $this->mqttClient->getValue($configTopic);
- if (!$configJson) {
- return ['success' => false, 'message' => 'Could not read chargepoint config'];
- }
-
- $config = json_decode($configJson, true);
- if (!$config || !isset($config['ev'])) {
- return ['success' => false, 'message' => 'No vehicle assigned to chargepoint'];
- }
-
- $vehicleId = $config['ev'];
- $topic = "openWB/set/vehicle/{$vehicleId}/soc_module/calculated_soc_state/manual_soc";
-
- if ($this->mqttClient->setValue($topic, strval($soc))) {
- return ['success' => true, 'message' => "Manual SoC set to {$soc}% for vehicle {$vehicleId} (chargepoint {$chargepointId})"];
- }
- return ['success' => false, 'message' => 'Failed to set manual SoC'];
- } catch (Exception $e) {
- return ['success' => false, 'message' => 'Error setting manual SoC: ' . $e->getMessage()];
- }
- }
-
- /**
- * Batterie-Modus setzen
- */
- private function setBatMode($value)
- {
- // Gültige Modi
- $validModes = ['min_soc_bat_mode', 'ev_mode', 'bat_mode'];
-
- if (!in_array($value, $validModes)) {
- return ['success' => false, 'message' => 'Invalid bat_mode. Valid modes: ' . implode(', ', $validModes)];
- }
-
- try {
- $topic = "openWB/set/general/chargemode_config/bat/mode";
-
- if ($this->mqttClient->setValue($topic, $value)) {
- return ['success' => true, 'message' => "Bat mode set to {$value}"];
- }
-
- return ['success' => false, 'message' => 'Failed to set bat mode'];
- } catch (Exception $e) {
- return ['success' => false, 'message' => 'Error setting bat mode: ' . $e->getMessage()];
- }
- }
-
- /**
- * Reservierte Batterie-Ladeleistung setzen (W)
- * Setzt openWB/set/general/chargemode_config/bat/power_reserve
- * Ermöglicht externes Energiemanagement der Speicherreservierung ohne UI-Eingriff.
- */
- private function setBatPowerReserve($value)
- {
- $watts = intval($value);
-
- if ($watts < 0) {
- return ['success' => false, 'message' => 'bat_power_reserve must be >= 0'];
- }
-
- try {
- $topic = "openWB/set/general/chargemode_config/bat/power_reserve";
-
- if ($this->mqttClient->setValue($topic, $watts)) {
- return [
- 'success' => true,
- 'message' => "bat_power_reserve set to {$watts}W",
- 'data' => ['bat_power_reserve' => $watts]
- ];
- }
-
- return ['success' => false, 'message' => 'Failed to set bat_power_reserve'];
- } catch (Exception $e) {
- return ['success' => false, 'message' => 'Error setting bat_power_reserve: ' . $e->getMessage()];
- }
- }
-
- /**
- * Lese openWB/system/lastlivevaluesJson Topic 1:1 aus
- */
- private function getLastLiveValuesJson()
- {
- $topic = "openWB/graph/lastlivevaluesJson";
- $jsonValue = $this->mqttClient->getValue($topic);
-
- if ($jsonValue === null) {
- return [
- 'get_lastlivevaluesjson' => null
- ];
- }
-
- // JSON-String 1:1 zurückgeben
- return [
- 'get_lastlivevaluesjson' => $jsonValue
- ];
- }
-
- /**
- * Counter Power
- */
- private function getCounterPower($id)
- {
- try {
- $topic = "openWB/counter/{$id}/get/power";
- $value = $this->mqttClient->getValue($topic);
- return ["counter_{$id}" => ['power' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["counter_{$id}" => ['power' => 0]];
- }
- }
-
- /**
- * Counter Powers
- */
- private function getCounterPowers($id)
- {
- try {
- $topic = "openWB/counter/{$id}/get/powers";
- $value = $this->mqttClient->getValue($topic);
- $powers = json_decode($value ?? '[]', true) ?: [0, 0, 0];
-
- return [
- "counter_{$id}" => [
- 'powers' => [
- floatval($powers[0] ?? 0),
- floatval($powers[1] ?? 0),
- floatval($powers[2] ?? 0)
- ]
- ]
- ];
- } catch (Exception $e) {
- return [
- "counter_{$id}" => [
- 'powers' => [0, 0, 0]
- ]
- ];
- }
- }
-
- /**
- * Counter Voltage P1
- */
- private function getCounterVoltageP1($id)
- {
- try {
- $topic = "openWB/counter/{$id}/get/voltages";
- $value = $this->mqttClient->getValue($topic);
- $voltages = json_decode($value ?? '[]', true) ?: [0, 0, 0];
- return ["counter_{$id}" => ['voltage_p1' => floatval($voltages[0] ?? 0)]];
- } catch (Exception $e) {
- return ["counter_{$id}" => ['voltage_p1' => 0]];
- }
- }
-
- /**
- * Counter Voltage P2
- */
- private function getCounterVoltageP2($id)
- {
- try {
- $topic = "openWB/counter/{$id}/get/voltages";
- $value = $this->mqttClient->getValue($topic);
- $voltages = json_decode($value ?? '[]', true) ?: [0, 0, 0];
- return ["counter_{$id}" => ['voltage_p2' => floatval($voltages[1] ?? 0)]];
- } catch (Exception $e) {
- return ["counter_{$id}" => ['voltage_p2' => 0]];
- }
- }
-
- /**
- * Counter Voltage P3
- */
- private function getCounterVoltageP3($id)
- {
- try {
- $topic = "openWB/counter/{$id}/get/voltages";
- $value = $this->mqttClient->getValue($topic);
- $voltages = json_decode($value ?? '[]', true) ?: [0, 0, 0];
- return ["counter_{$id}" => ['voltage_p3' => floatval($voltages[2] ?? 0)]];
- } catch (Exception $e) {
- return ["counter_{$id}" => ['voltage_p3' => 0]];
- }
- }
-
- /**
- * Counter Voltages
- */
- private function getCounterVoltages($id)
- {
- try {
- $topic = "openWB/counter/{$id}/get/voltages";
- $value = $this->mqttClient->getValue($topic);
- $voltages = json_decode($value ?? '[]', true) ?: [0, 0, 0];
-
- return [
- "counter_{$id}" => [
- 'voltages' => [
- floatval($voltages[0] ?? 0),
- floatval($voltages[1] ?? 0),
- floatval($voltages[2] ?? 0)
- ]
- ]
- ];
- } catch (Exception $e) {
- return [
- "counter_{$id}" => [
- 'voltages' => [0, 0, 0]
- ]
- ];
- }
- }
-
- /**
- * Counter Current P1
- */
- private function getCounterCurrentP1($id)
- {
- try {
- $topic = "openWB/counter/{$id}/get/currents";
- $value = $this->mqttClient->getValue($topic);
- $currents = json_decode($value ?? '[]', true) ?: [0, 0, 0];
- return ["counter_{$id}" => ['current_p1' => floatval($currents[0] ?? 0)]];
- } catch (Exception $e) {
- return ["counter_{$id}" => ['current_p1' => 0]];
- }
- }
-
- /**
- * Counter Current P2
- */
- private function getCounterCurrentP2($id)
- {
- try {
- $topic = "openWB/counter/{$id}/get/currents";
- $value = $this->mqttClient->getValue($topic);
- $currents = json_decode($value ?? '[]', true) ?: [0, 0, 0];
- return ["counter_{$id}" => ['current_p2' => floatval($currents[1] ?? 0)]];
- } catch (Exception $e) {
- return ["counter_{$id}" => ['current_p2' => 0]];
- }
- }
-
- /**
- * Counter Current P3
- */
- private function getCounterCurrentP3($id)
- {
- try {
- $topic = "openWB/counter/{$id}/get/currents";
- $value = $this->mqttClient->getValue($topic);
- $currents = json_decode($value ?? '[]', true) ?: [0, 0, 0];
- return ["counter_{$id}" => ['current_p3' => floatval($currents[2] ?? 0)]];
- } catch (Exception $e) {
- return ["counter_{$id}" => ['current_p3' => 0]];
- }
- }
-
- /**
- * Counter Currents
- */
- private function getCounterCurrents($id)
- {
- try {
- $topic = "openWB/counter/{$id}/get/currents";
- $value = $this->mqttClient->getValue($topic);
- $currents = json_decode($value ?? '[]', true) ?: [0, 0, 0];
-
- return [
- "counter_{$id}" => [
- 'currents' => [
- floatval($currents[0] ?? 0),
- floatval($currents[1] ?? 0),
- floatval($currents[2] ?? 0)
- ]
- ]
- ];
- } catch (Exception $e) {
- return [
- "counter_{$id}" => [
- 'currents' => [0, 0, 0]
- ]
- ];
- }
- }
-
- /**
- * Counter Power Factors
- */
- private function getCounterPowerFactors($id)
- {
- try {
- $topic = "openWB/counter/{$id}/get/power_factors";
- $value = $this->mqttClient->getValue($topic);
- $powerFactors = json_decode($value ?? '[]', true) ?: [0, 0, 0];
-
- return [
- "counter_{$id}" => [
- 'power_factors' => [
- floatval($powerFactors[0] ?? 0),
- floatval($powerFactors[1] ?? 0),
- floatval($powerFactors[2] ?? 0)
- ]
- ]
- ];
- } catch (Exception $e) {
- return [
- "counter_{$id}" => [
- 'power_factors' => [0, 0, 0]
- ]
- ];
- }
- }
-
- /**
- * Counter Imported
- */
- private function getCounterImported($id)
- {
- try {
- $topic = "openWB/counter/{$id}/get/imported";
- $value = $this->mqttClient->getValue($topic);
- return ["counter_{$id}" => ['imported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["counter_{$id}" => ['imported' => 0]];
- }
- }
-
- /**
- * Counter Exported
- */
- private function getCounterExported($id)
- {
- try {
- $topic = "openWB/counter/{$id}/get/exported";
- $value = $this->mqttClient->getValue($topic);
- return ["counter_{$id}" => ['exported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["counter_{$id}" => ['exported' => 0]];
- }
- }
-
- /**
- * Counter Daily Imported
- */
- private function getCounterDailyImported($id)
- {
- try {
- $topic = "openWB/counter/{$id}/get/daily_imported";
- $value = $this->mqttClient->getValue($topic);
- return ["counter_{$id}" => ['daily_imported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["counter_{$id}" => ['daily_imported' => 0]];
- }
- }
-
- /**
- * Counter Daily Exported
- */
- private function getCounterDailyExported($id)
- {
- try {
- $topic = "openWB/counter/{$id}/get/daily_exported";
- $value = $this->mqttClient->getValue($topic);
- return ["counter_{$id}" => ['daily_exported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["counter_{$id}" => ['daily_exported' => 0]];
- }
- }
-
- /**
- * Counter Frequency
- */
- private function getCounterFrequency($id)
- {
- try {
- $topic = "openWB/counter/{$id}/get/frequency";
- $value = $this->mqttClient->getValue($topic);
- return ["counter_{$id}" => ['frequency' => floatval($value ?? 50.0)]];
- } catch (Exception $e) {
- return ["counter_{$id}" => ['frequency' => 50.0]];
- }
- }
-
- /**
- * Counter Fault String
- */
- private function getCounterFaultStr($id)
- {
- try {
- $topic = "openWB/counter/{$id}/get/fault_str";
- $value = $this->mqttClient->getValue($topic);
- return ["counter_{$id}" => ['fault_str' => $this->decodeStringValue($value, 'Kein Fehler')]];
- } catch (Exception $e) {
- return ["counter_{$id}" => ['fault_str' => 'Kein Fehler']];
- }
- }
-
- /**
- * Counter Fault State
- */
- private function getCounterFaultState($id)
- {
- try {
- $topic = "openWB/counter/{$id}/get/fault_state";
- $value = $this->mqttClient->getValue($topic);
- return ["counter_{$id}" => ['fault_state' => intval($value ?? 0)]];
- } catch (Exception $e) {
- return ["counter_{$id}" => ['fault_state' => 0]];
- }
- }
-
- /**
- * Battery Power
- */
- private function getBatteryPower($id)
- {
- try {
- $topic = "openWB/bat/{$id}/get/power";
- $value = $this->mqttClient->getValue($topic);
- return ["battery_{$id}" => ['power' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["battery_{$id}" => ['power' => 0]];
- }
- }
-
- /**
- * Battery SoC
- */
- private function getBatterySoc($id)
- {
- try {
- $topic = "openWB/bat/{$id}/get/soc";
- $value = $this->mqttClient->getValue($topic);
- return ["battery_{$id}" => ['soc' => intval($value ?? 0)]];
- } catch (Exception $e) {
- return ["battery_{$id}" => ['soc' => 0]];
- }
- }
-
- /**
- * Battery Currents
- */
- private function getBatteryCurrents($id)
- {
- try {
- $topic = "openWB/bat/{$id}/get/currents";
- $value = $this->mqttClient->getValue($topic);
- $currents = json_decode($value ?? '[]', true) ?: [0, 0, 0];
-
- return [
- "battery_{$id}" => [
- 'currents' => [
- floatval($currents[0] ?? 0),
- floatval($currents[1] ?? 0),
- floatval($currents[2] ?? 0)
- ]
- ]
- ];
- } catch (Exception $e) {
- return [
- "battery_{$id}" => [
- 'currents' => [0, 0, 0]
- ]
- ];
- }
- }
-
- /**
- * Battery Imported
- */
- private function getBatteryImported($id)
- {
- try {
- $topic = "openWB/bat/{$id}/get/imported";
- $value = $this->mqttClient->getValue($topic);
- return ["battery_{$id}" => ['imported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["battery_{$id}" => ['imported' => 0]];
- }
- }
-
- /**
- * Battery Exported
- */
- private function getBatteryExported($id)
- {
- try {
- $topic = "openWB/bat/{$id}/get/exported";
- $value = $this->mqttClient->getValue($topic);
- return ["battery_{$id}" => ['exported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["battery_{$id}" => ['exported' => 0]];
- }
- }
-
- /**
- * Battery Daily Imported
- */
- private function getBatteryDailyImported($id)
- {
- try {
- $topic = "openWB/bat/{$id}/get/daily_imported";
- $value = $this->mqttClient->getValue($topic);
- return ["battery_{$id}" => ['daily_imported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["battery_{$id}" => ['daily_imported' => 0]];
- }
- }
-
- /**
- * Battery Daily Exported
- */
- private function getBatteryDailyExported($id)
- {
- try {
- $topic = "openWB/bat/{$id}/get/daily_exported";
- $value = $this->mqttClient->getValue($topic);
- return ["battery_{$id}" => ['daily_exported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["battery_{$id}" => ['daily_exported' => 0]];
- }
- }
-
- /**
- * Battery Fault String
- */
- private function getBatteryFaultStr($id)
- {
- try {
- $topic = "openWB/bat/{$id}/get/fault_str";
- $value = $this->mqttClient->getValue($topic);
- return ["battery_{$id}" => ['fault_str' => $this->decodeStringValue($value, 'Kein Fehler')]];
- } catch (Exception $e) {
- return ["battery_{$id}" => ['fault_str' => 'Kein Fehler']];
- }
- }
-
- /**
- * Battery Fault State
- */
- private function getBatteryFaultState($id)
- {
- try {
- $topic = "openWB/bat/{$id}/get/fault_state";
- $value = $this->mqttClient->getValue($topic);
- return ["battery_{$id}" => ['fault_state' => intval($value ?? 0)]];
- } catch (Exception $e) {
- return ["battery_{$id}" => ['fault_state' => 0]];
- }
- }
-
- /**
- * Battery Power Limit Controllable
- */
- private function getBatteryPowerLimitControllable($id)
- {
- try {
- $topic = "openWB/bat/{$id}/get/power_limit_controllable";
- $value = $this->mqttClient->getValue($topic);
- return ["battery_{$id}" => ['power_limit_controllable' => boolval($value ?? false)]];
- } catch (Exception $e) {
- return ["battery_{$id}" => ['power_limit_controllable' => false]];
- }
- }
-
- /**
- * PV Power
- */
- private function getPvPower($id)
- {
- if ($this->isTotalId($id)) {
- $sum = $this->getTotalMetricValue('pv', 'power');
- return ['pv_total' => ['power' => $sum]];
- }
-
- try {
- $topic = "openWB/pv/{$id}/get/power";
- $value = $this->mqttClient->getValue($topic);
- return ["pv_{$id}" => ['power' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["pv_{$id}" => ['power' => 0]];
- }
- }
-
- /**
- * PV Currents
- */
- private function getPvCurrents($id)
- {
- try {
- $topic = "openWB/pv/{$id}/get/currents";
- $value = $this->mqttClient->getValue($topic);
- $currents = json_decode($value ?? '[]', true) ?: [0, 0, 0];
-
- return [
- "pv_{$id}" => [
- 'currents' => [
- floatval($currents[0] ?? 0),
- floatval($currents[1] ?? 0),
- floatval($currents[2] ?? 0)
- ]
- ]
- ];
- } catch (Exception $e) {
- return [
- "pv_{$id}" => [
- 'currents' => [0, 0, 0]
- ]
- ];
- }
- }
-
- /**
- * PV Imported
- */
- private function getPvImported($id)
- {
- if ($this->isTotalId($id)) {
- $sum = $this->getTotalMetricValue('pv', 'imported');
- return ['pv_total' => ['imported' => $sum]];
- }
-
- try {
- $topic = "openWB/pv/{$id}/get/imported";
- $value = $this->mqttClient->getValue($topic);
- return ["pv_{$id}" => ['imported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["pv_{$id}" => ['imported' => 0]];
- }
- }
-
- /**
- * PV Exported
- */
- private function getPvExported($id)
- {
- if ($this->isTotalId($id)) {
- $sum = $this->getTotalMetricValue('pv', 'exported');
- return ['pv_total' => ['exported' => $sum]];
- }
-
- try {
- $topic = "openWB/pv/{$id}/get/exported";
- $value = $this->mqttClient->getValue($topic);
- return ["pv_{$id}" => ['exported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["pv_{$id}" => ['exported' => 0]];
- }
- }
-
- /**
- * PV Daily Exported
- */
- private function getPvDailyExported($id)
- {
- try {
- $topic = "openWB/pv/{$id}/get/daily_exported";
- $value = $this->mqttClient->getValue($topic);
- return ["pv_{$id}" => ['daily_exported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["pv_{$id}" => ['daily_exported' => 0]];
- }
- }
-
- /**
- * PV Monthly Exported
- */
- private function getPvMonthlyExported($id)
- {
- if ($this->isTotalId($id)) {
- $sum = $this->getTotalMetricValue('pv', 'monthly_exported');
- return ['pv_total' => ['monthly_exported' => $sum]];
- }
-
- try {
- $topic = "openWB/pv/{$id}/get/monthly_exported";
- $value = $this->mqttClient->getValue($topic);
- return ["pv_{$id}" => ['monthly_exported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["pv_{$id}" => ['monthly_exported' => 0]];
- }
- }
-
- /**
- * PV Yearly Exported
- */
- private function getPvYearlyExported($id)
- {
- if ($this->isTotalId($id)) {
- $sum = $this->getTotalMetricValue('pv', 'yearly_exported');
- return ['pv_total' => ['yearly_exported' => $sum]];
- }
-
- try {
- $topic = "openWB/pv/{$id}/get/yearly_exported";
- $value = $this->mqttClient->getValue($topic);
- return ["pv_{$id}" => ['yearly_exported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["pv_{$id}" => ['yearly_exported' => 0]];
- }
- }
-
- /**
- * PV Fault String
- */
- private function getPvFaultStr($id)
- {
- try {
- $topic = "openWB/pv/{$id}/get/fault_str";
- $value = $this->mqttClient->getValue($topic);
- return ["pv_{$id}" => ['fault_str' => $this->decodeStringValue($value, 'Kein Fehler')]];
- } catch (Exception $e) {
- return ["pv_{$id}" => ['fault_str' => 'Kein Fehler']];
- }
- }
-
- /**
- * PV Fault State
- */
- private function getPvFaultState($id)
- {
- try {
- $topic = "openWB/pv/{$id}/get/fault_state";
- $value = $this->mqttClient->getValue($topic);
- return ["pv_{$id}" => ['fault_state' => intval($value ?? 0)]];
- } catch (Exception $e) {
- return ["pv_{$id}" => ['fault_state' => 0]];
- }
- }
-
- /**
- * Consumer Power
- */
- private function getConsumerPower($id)
- {
- try {
- $topic = "openWB/consumer/{$id}/get/power";
- $value = $this->mqttClient->getValue($topic);
- return ["consumer_{$id}" => ['power' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["consumer_{$id}" => ['power' => 0]];
- }
- }
-
- /**
- * Consumer Currents
- */
- private function getConsumerCurrents($id)
- {
- try {
- $topic = "openWB/consumer/{$id}/get/currents";
- $value = $this->mqttClient->getValue($topic);
- $currents = json_decode($value ?? '[]', true) ?: [0, 0, 0];
-
- return [
- "consumer_{$id}" => [
- 'currents' => [
- floatval($currents[0] ?? 0),
- floatval($currents[1] ?? 0),
- floatval($currents[2] ?? 0)
- ]
- ]
- ];
- } catch (Exception $e) {
- return ["consumer_{$id}" => ['currents' => [0, 0, 0]]];
- }
- }
-
- /**
- * Consumer Voltages
- */
- private function getConsumerVoltages($id)
- {
- try {
- $topic = "openWB/consumer/{$id}/get/voltages";
- $value = $this->mqttClient->getValue($topic);
- $voltages = json_decode($value ?? '[]', true) ?: [0, 0, 0];
-
- return [
- "consumer_{$id}" => [
- 'voltages' => [
- floatval($voltages[0] ?? 0),
- floatval($voltages[1] ?? 0),
- floatval($voltages[2] ?? 0)
- ]
- ]
- ];
- } catch (Exception $e) {
- return ["consumer_{$id}" => ['voltages' => [0, 0, 0]]];
- }
- }
-
- /**
- * Consumer Powers
- */
- private function getConsumerPowers($id)
- {
- try {
- $topic = "openWB/consumer/{$id}/get/powers";
- $value = $this->mqttClient->getValue($topic);
- $powers = json_decode($value ?? '[]', true) ?: [0, 0, 0];
-
- return [
- "consumer_{$id}" => [
- 'powers' => [
- floatval($powers[0] ?? 0),
- floatval($powers[1] ?? 0),
- floatval($powers[2] ?? 0)
- ]
- ]
- ];
- } catch (Exception $e) {
- return ["consumer_{$id}" => ['powers' => [0, 0, 0]]];
- }
- }
-
- /**
- * Consumer Imported
- */
- private function getConsumerImported($id)
- {
- try {
- $topic = "openWB/consumer/{$id}/get/imported";
- $value = $this->mqttClient->getValue($topic);
- return ["consumer_{$id}" => ['imported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["consumer_{$id}" => ['imported' => 0]];
- }
- }
-
- /**
- * Consumer Exported
- */
- private function getConsumerExported($id)
- {
- try {
- $topic = "openWB/consumer/{$id}/get/exported";
- $value = $this->mqttClient->getValue($topic);
- return ["consumer_{$id}" => ['exported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["consumer_{$id}" => ['exported' => 0]];
- }
- }
-
- /**
- * Consumer Daily Imported
- */
- private function getConsumerDailyImported($id)
- {
- try {
- $topic = "openWB/consumer/{$id}/get/daily_imported";
- $value = $this->mqttClient->getValue($topic);
- return ["consumer_{$id}" => ['daily_imported' => floatval($value ?? 0)]];
- } catch (Exception $e) {
- return ["consumer_{$id}" => ['daily_imported' => 0]];
- }
- }
-
- /**
- * Consumer Phases In Use
- */
- private function getConsumerPhasesInUse($id)
- {
- try {
- $topic = "openWB/consumer/{$id}/get/phases_in_use";
- $value = $this->mqttClient->getValue($topic);
- return ["consumer_{$id}" => ['phases_in_use' => intval($value ?? 0)]];
- } catch (Exception $e) {
- return ["consumer_{$id}" => ['phases_in_use' => 0]];
- }
- }
-
- /**
- * Consumer Charge State
- */
- private function getConsumerChargeState($id)
- {
- try {
- $topic = "openWB/consumer/{$id}/get/charge_state";
- $value = $this->mqttClient->getValue($topic);
- return ["consumer_{$id}" => ['charge_state' => $this->parseBooleanValue($value ?? 'false')]];
- } catch (Exception $e) {
- return ["consumer_{$id}" => ['charge_state' => false]];
- }
- }
-
- /**
- * Consumer State String
- */
- private function getConsumerStateStr($id)
- {
- try {
- $topic = "openWB/consumer/{$id}/get/state_str";
- $value = $this->mqttClient->getValue($topic);
- return ["consumer_{$id}" => ['state_str' => $this->decodeStringValue($value, null)]];
- } catch (Exception $e) {
- return ["consumer_{$id}" => ['state_str' => null]];
- }
- }
-
- /**
- * Consumer Fault String
- */
- private function getConsumerFaultStr($id)
- {
- try {
- $topic = "openWB/consumer/{$id}/get/fault_str";
- $value = $this->mqttClient->getValue($topic);
- return ["consumer_{$id}" => ['fault_str' => $this->decodeStringValue($value, 'Kein Fehler')]];
- } catch (Exception $e) {
- return ["consumer_{$id}" => ['fault_str' => 'Kein Fehler']];
- }
- }
-
- /**
- * Consumer Fault State
- */
- private function getConsumerFaultState($id)
- {
- try {
- $topic = "openWB/consumer/{$id}/get/fault_state";
- $value = $this->mqttClient->getValue($topic);
- return ["consumer_{$id}" => ['fault_state' => intval($value ?? 0)]];
- } catch (Exception $e) {
- return ["consumer_{$id}" => ['fault_state' => 0]];
- }
- }
-
- /**
- * Consumer Usage Type
- */
- private function getConsumerUsageType($id)
- {
- try {
- $topic = "openWB/consumer/{$id}/usage";
- $value = $this->mqttClient->getValue($topic);
- $usage = json_decode($value ?? '{}', true);
- return ["consumer_{$id}" => ['usage_type' => $usage['type'] ?? null]];
- } catch (Exception $e) {
- return ["consumer_{$id}" => ['usage_type' => null]];
- }
- }
-
- /**
- * Hilfsmethode für numerische Werte
- */
- private function getNumericValue($topic)
- {
- try {
- $value = $this->mqttClient->getValue($topic);
- return floatval($value ?? 0);
- } catch (Exception $e) {
- return 0;
- }
- }
-
- /**
- * IO-Ausgaenge komplett lesen (digital + analog).
- */
- private function getIoOutputAll($id)
- {
- $digital = $this->getIoOutputMap($id, 'digital_output');
- $analog = $this->getIoOutputMap($id, 'analog_output');
-
- return [
- "io_{$id}" => [
- 'digital_output' => $digital,
- 'analog_output' => $analog
- ]
- ];
- }
-
- /**
- * Einzelnen IO-Ausgang lesen.
- */
- private function getIoOutput($id, $outputName, $outputType = null)
- {
- if ($outputName === null || trim((string)$outputName) === '') {
- return $this->getIoOutputAll($id);
- }
-
- $outputName = trim((string)$outputName);
- $type = $this->normalizeIoOutputType($outputType);
- $result = null;
-
- if ($type === 'digital_output' || $type === null) {
- $digital = $this->getIoOutputMap($id, 'digital_output');
- if (array_key_exists($outputName, $digital)) {
- $result = $digital[$outputName];
- $type = 'digital_output';
- }
- }
-
- if ($result === null && ($type === 'analog_output' || $type === null)) {
- $analog = $this->getIoOutputMap($id, 'analog_output');
- if (array_key_exists($outputName, $analog)) {
- $result = $analog[$outputName];
- $type = 'analog_output';
- }
- }
-
- return [
- "io_{$id}" => [
- 'output' => $outputName,
- 'output_type' => $type,
- 'value' => $result
- ]
- ];
- }
-
- /**
- * IO-Ausgang manuell setzen.
- */
- private function setIoOutput($id, $value, $outputName, $outputType = null)
- {
- if ($id === null || $id === '') {
- return ['success' => false, 'message' => 'Missing io_nr'];
- }
-
- if ($outputName === null || trim((string)$outputName) === '') {
- return ['success' => false, 'message' => 'Missing io_output'];
- }
-
- $outputName = trim((string)$outputName);
- $normalizedType = $this->normalizeIoOutputType($outputType);
- $parsedValue = $this->parseIoOutputValue($value);
-
- if ($normalizedType === null) {
- // Automatisch anhand des aktuellen Status bestimmen.
- $digital = $this->getIoOutputMap($id, 'digital_output');
- $analog = $this->getIoOutputMap($id, 'analog_output');
-
- if (array_key_exists($outputName, $digital)) {
- $normalizedType = 'digital_output';
- } elseif (array_key_exists($outputName, $analog)) {
- $normalizedType = 'analog_output';
- } elseif (is_bool($parsedValue)) {
- $normalizedType = 'digital_output';
- } else {
- $normalizedType = 'analog_output';
- }
- }
-
- $topic = "openWB/set/system/io/{$id}/set/manual/{$normalizedType}/{$outputName}";
-
- if ($normalizedType === 'digital_output') {
- if (is_bool($parsedValue)) {
- $payload = $parsedValue ? 'true' : 'false';
- } elseif (is_numeric($parsedValue) && (float)$parsedValue === 0.0) {
- $payload = 'false';
- } elseif (is_numeric($parsedValue) && (float)$parsedValue === 1.0) {
- $payload = 'true';
- } else {
- return ['success' => false, 'message' => 'Digital output expects boolean or 0/1'];
- }
- } else {
- if (is_bool($parsedValue)) {
- $payload = $parsedValue ? '1' : '0';
- } else {
- $payload = (string)$parsedValue;
- }
- }
-
- try {
- $this->mqttClient->setValue($topic, $payload);
- return [
- 'success' => true,
- 'message' => "Set {$normalizedType}/{$outputName} to {$payload} for io {$id}",
- 'data' => [
- 'io_nr' => intval($id),
- 'io_output' => $outputName,
- 'io_output_type' => $normalizedType,
- 'value' => $parsedValue
- ]
- ];
- } catch (Exception $e) {
- return ['success' => false, 'message' => 'Failed to set IO output: ' . $e->getMessage()];
- }
- }
-
- /**
- * IO-Output-Map von MQTT laden.
- */
- private function getIoOutputMap($id, $outputType)
- {
- try {
- $topic = "openWB/io/states/{$id}/get/{$outputType}";
- $value = $this->mqttClient->getValue($topic);
- $decoded = json_decode($value ?? '{}', true);
- return is_array($decoded) ? $decoded : [];
- } catch (Exception $e) {
- return [];
- }
- }
-
- /**
- * Normiert den optionalen io_output_type Parameter.
- */
- private function normalizeIoOutputType($outputType)
- {
- if ($outputType === null || trim((string)$outputType) === '') {
- return null;
- }
-
- $value = strtolower(trim((string)$outputType));
- if ($value === 'digital' || $value === 'digital_output') {
- return 'digital_output';
- }
- if ($value === 'analog' || $value === 'analog_output') {
- return 'analog_output';
- }
-
- return null;
- }
-
- /**
- * Wandelt Eingaben fuer IO-Werte in bool/float um.
- */
- private function parseIoOutputValue($value)
- {
- if (is_bool($value)) {
- return $value;
- }
-
- if (is_numeric($value)) {
- return floatval($value);
- }
-
- $normalized = strtolower(trim((string)$value));
- if (in_array($normalized, ['true', 'on', 'yes'], true)) {
- return true;
- }
- if (in_array($normalized, ['false', 'off', 'no'], true)) {
- return false;
- }
-
- if (is_numeric($normalized)) {
- return floatval($normalized);
- }
-
- return $value;
- }
-
- /**
- * Prüft, ob eine total-Abfrage angefordert wurde.
- */
- private function isTotalId($id)
- {
- return is_string($id) && strtolower(trim($id)) === 'total';
- }
-
- /**
- * Summiert einen Metrik-Wert über alle vorhandenen IDs per MQTT-Wildcard.
- */
- private function getTotalMetricValue($componentType, $metric)
- {
- try {
- $pattern = "openWB/{$componentType}/+/get/{$metric}";
- $values = $this->mqttClient->getValuesByWildcard($pattern);
-
- $sum = 0.0;
- foreach ($values as $value) {
- if (is_numeric($value)) {
- $sum += floatval($value);
- }
- }
-
- return $sum;
- } catch (Exception $e) {
- return 0;
- }
- }
-}
diff --git a/simpleAPI/src/authenticator.py b/simpleAPI/src/authenticator.py
new file mode 100644
index 0000000000..5b6f050c09
--- /dev/null
+++ b/simpleAPI/src/authenticator.py
@@ -0,0 +1,119 @@
+"""Four-tier authentication for the simpleAPI HTTP server.
+
+Precedence: bearer token, local username/password, bare token parameter,
+then a fallback that delegates to the MQTT broker itself - if the broker
+accepts anonymous connections, simpleAPI is effectively open too.
+"""
+import hmac
+import logging
+import time
+from dataclasses import dataclass, field
+from typing import Callable, Dict, List, Optional
+
+import paho.mqtt.client as mqtt
+from passlib.hash import bcrypt
+
+log = logging.getLogger("simpleAPI")
+
+
+@dataclass
+class AuthConfig:
+ enabled: bool = False
+ require_https: bool = False
+ tokens: List[str] = field(default_factory=list)
+ users: Dict[str, str] = field(default_factory=dict)
+
+ @classmethod
+ def from_dict(cls, data: Dict) -> "AuthConfig":
+ return cls(
+ enabled=data.get('enabled', False),
+ require_https=data.get('require_https', False),
+ tokens=list(data.get('tokens', [])),
+ users=dict(data.get('users', {})),
+ )
+
+
+def test_mqtt_credentials(
+ host: str, port: int, use_tls: bool, validate_cert: bool,
+ username: str, password: str, timeout: float = 3.0) -> bool:
+ """Probe the broker with a short-lived connection instead of shelling out to
+ mosquitto_sub. An empty username/password tests anonymous access."""
+ outcome: Dict[str, Optional[int]] = {'rc': None}
+
+ def on_connect(client, userdata, flags, rc):
+ outcome['rc'] = rc
+
+ client = mqtt.Client()
+ client.on_connect = on_connect
+ if use_tls:
+ client.tls_set(cert_reqs=__import__('ssl').CERT_REQUIRED)
+ if not validate_cert:
+ client.tls_insecure_set(True)
+ if username:
+ client.username_pw_set(username, password)
+
+ try:
+ client.connect(host, port, keepalive=max(int(timeout), 1))
+ except Exception as exc:
+ log.debug(f"MQTT credential probe failed to connect: {exc}")
+ return False
+
+ client.loop_start()
+ deadline = time.monotonic() + timeout
+ try:
+ while outcome['rc'] is None and time.monotonic() < deadline:
+ time.sleep(0.05)
+ finally:
+ client.loop_stop()
+ try:
+ client.disconnect()
+ except Exception:
+ pass
+
+ return outcome['rc'] == 0
+
+
+class Authenticator:
+ def __init__(self, config: AuthConfig, mqtt_params: Dict,
+ credential_tester: Callable[..., bool] = test_mqtt_credentials):
+ self.config = config
+ self.mqtt_params = mqtt_params
+ self._credential_tester = credential_tester
+
+ def authenticate(self, params: Dict[str, str], bearer_token: Optional[str], is_https: bool) -> bool:
+ if not self.config.enabled:
+ return True
+
+ if bearer_token and self._validate_token(bearer_token):
+ return True
+
+ username = params.get('username')
+ password = params.get('password')
+ if username is not None and password is not None:
+ if (is_https or not self.config.require_https) and self._validate_credentials(username, password):
+ return True
+
+ token = params.get('token')
+ if token is not None:
+ if (is_https or not self.config.require_https) and self._validate_token(token):
+ return True
+
+ return self._test_broker_anonymous()
+
+ def _validate_token(self, token: str) -> bool:
+ return any(hmac.compare_digest(valid, token) for valid in self.config.tokens)
+
+ def _validate_credentials(self, username: str, password: str) -> bool:
+ users = self.config.users
+ if users and username in users:
+ stored = users[username]
+ if stored.startswith('$'):
+ try:
+ return bcrypt.verify(password, stored)
+ except ValueError:
+ return False
+ return hmac.compare_digest(stored, password)
+ return self._credential_tester(username=username, password=password, **self.mqtt_params)
+
+ def _test_broker_anonymous(self) -> bool:
+ return self._credential_tester(username='', password='', **self.mqtt_params)
diff --git a/simpleAPI/src/http_server.py b/simpleAPI/src/http_server.py
new file mode 100644
index 0000000000..4bc552241e
--- /dev/null
+++ b/simpleAPI/src/http_server.py
@@ -0,0 +1,178 @@
+"""HTTP surface for simpleAPI: request parsing, auth, and the read/write/raw
+response contract that third-party integrations (Home Assistant, node-red)
+already rely on."""
+import json
+import logging
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+from typing import Any, Dict
+from urllib.parse import parse_qs, urlsplit
+
+from .authenticator import AuthConfig, Authenticator
+from .parameter_handler import (
+ ParameterHandler,
+ dispatch_read,
+ dispatch_write,
+ extract_read_params,
+ extract_write_params,
+ format_raw_output,
+ COMPLEX_PARAMETERS,
+)
+
+log = logging.getLogger("simpleAPI")
+
+
+def _php_stringify(value: Any) -> str:
+ """Approximates PHP's echo-time string conversion for raw-mode output
+ (bool True/False -> "1"/"", None -> "", arrays -> "Array")."""
+ if value is None:
+ return ''
+ if isinstance(value, bool):
+ return '1' if value else ''
+ if isinstance(value, (dict, list)):
+ return 'Array'
+ return str(value)
+
+
+def _single_param_value(params: Dict[str, list]) -> Dict[str, str]:
+ return {k: (v[-1] if v else '') for k, v in params.items()}
+
+
+class SimpleAPIRequestHandler(BaseHTTPRequestHandler):
+ server_version = "openWB-simpleAPI/1.0"
+
+ def log_message(self, fmt, *args):
+ log.debug(f"{self.address_string()} - {fmt % args}")
+
+ def _is_https(self) -> bool:
+ return self.headers.get('X-Forwarded-Proto', '').lower() == 'https'
+
+ def _bearer_token(self):
+ auth_header = self.headers.get('Authorization')
+ if not auth_header:
+ return None
+ prefix = 'bearer '
+ if auth_header.lower().startswith(prefix):
+ return auth_header[len(prefix):].strip()
+ return None
+
+ def _send(self, status: int, payload: Any, raw: bool = False):
+ if raw:
+ body = payload if isinstance(payload, (bytes, bytearray)) else _php_stringify(payload).encode('utf-8')
+ else:
+ body = json.dumps(payload).encode('utf-8')
+
+ self.send_response(status)
+ self.send_header('Content-Type', 'application/json')
+ if self.server.api_config.get('cors_enabled', False):
+ self.send_header('Access-Control-Allow-Origin', '*')
+ self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
+ self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
+ self.send_header('Content-Length', str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+
+ def do_OPTIONS(self):
+ self._send(200, '', raw=True)
+
+ def do_GET(self):
+ params = _single_param_value(parse_qs(urlsplit(self.path).query, keep_blank_values=True))
+ self._handle(params)
+
+ def do_POST(self):
+ length = int(self.headers.get('Content-Length') or 0)
+ body = self.rfile.read(length) if length else b''
+ post_params = _single_param_value(parse_qs(body.decode('utf-8'), keep_blank_values=True))
+ get_params = _single_param_value(parse_qs(urlsplit(self.path).query, keep_blank_values=True))
+ merged = {**get_params, **post_params}
+ self._handle(merged)
+
+ def _handle(self, params: Dict[str, str]):
+ debug = bool(self.server.config.get('debug', False)) or params.get('debug') == 'true'
+ try:
+ if not self.server.authenticator.authenticate(params, self._bearer_token(), self._is_https()):
+ self._send(401, {'success': False, 'message': 'Authentication failed'})
+ return
+
+ if 'list_components' in params:
+ self._send(200, self.server.parameter_handler.list_components())
+ return
+
+ write_params = extract_write_params(params)
+ if write_params:
+ result = dispatch_write(self.server.parameter_handler, write_params, params)
+ if params.get('raw') == 'true':
+ text = 'OK' if result['success'] else f"ERROR: {result.get('message', 'Unknown error')}"
+ self._send(200, text, raw=True)
+ else:
+ self._send(200, result)
+ return
+
+ read_params = extract_read_params(params)
+ if read_params:
+ result, debug_info = dispatch_read(self.server.parameter_handler, read_params, params, debug)
+ if debug and debug_info:
+ result = {'debug_info': debug_info, **result}
+
+ if params.get('raw') == 'true':
+ if len(read_params) > 1:
+ self._send(400, {
+ 'success': False,
+ 'message': 'Raw output (raw=true) is only allowed for single parameter requests',
+ 'error': 'Multiple parameters detected: ' + ', '.join(read_params.keys()),
+ })
+ return
+ param_name = next(iter(read_params))
+ if param_name in COMPLEX_PARAMETERS:
+ self._send(400, {
+ 'success': False,
+ 'message': 'Raw output (raw=true) is not supported for complex parameters',
+ 'error': f"Parameter '{param_name}' returns multiple values. "
+ "Use specific single-value parameters instead.",
+ 'suggestion': 'Try parameters like get_counter_power, get_counter_voltage_p1, etc.',
+ })
+ return
+ self._send(200, format_raw_output(result, param_name), raw=True)
+ else:
+ if not result:
+ self._send(200, [])
+ else:
+ self._send(200, result)
+ return
+
+ self._send(400, {'success': False, 'message': 'No valid parameters provided'})
+ except Exception as exc:
+ log.exception("Unhandled error processing simpleAPI request")
+ self._send(500, {
+ 'success': False,
+ 'message': 'Internal server error',
+ 'error': str(exc) if debug else 'An error occurred',
+ })
+
+
+class SimpleAPIServer(ThreadingHTTPServer):
+ def __init__(self, server_address, daemon, parameter_handler: ParameterHandler,
+ authenticator: Authenticator, config: Dict):
+ super().__init__(server_address, SimpleAPIRequestHandler)
+ self.daemon_ref = daemon
+ self.parameter_handler = parameter_handler
+ self.authenticator = authenticator
+ self.config = config
+ self.api_config = config.get('api', {})
+
+
+def build_http_server(daemon, config: Dict) -> SimpleAPIServer:
+ http_config = config.get('http', {})
+ host = http_config.get('host', '127.0.0.1')
+ port = http_config.get('port', 8099)
+
+ auth_config = AuthConfig.from_dict(config.get('auth', {}))
+ mqtt_params = {
+ 'host': config['host'],
+ 'port': config['port'],
+ 'use_tls': config['use_tls'],
+ 'validate_cert': config.get('validate_cert', False),
+ }
+ authenticator = Authenticator(auth_config, mqtt_params)
+ parameter_handler = ParameterHandler(daemon)
+
+ return SimpleAPIServer((host, port), daemon, parameter_handler, authenticator, config)
diff --git a/simpleAPI/src/parameter_handler.py b/simpleAPI/src/parameter_handler.py
new file mode 100644
index 0000000000..bb4aec9857
--- /dev/null
+++ b/simpleAPI/src/parameter_handler.py
@@ -0,0 +1,733 @@
+"""Read/write parameter dispatch for the simpleAPI HTTP server.
+
+Mirrors the parameter whitelist, auto-ID resolution and response-shape rules
+of the HTTP dispatch layer, and reads every value straight out of the running
+daemon's caches - no MQTT round-trip is made per request.
+"""
+from typing import Any, Callable, Dict, List, Optional, Tuple
+
+# Write parameters, in the order they are evaluated when several are present
+# in one request (the first one in this order determines the response's
+# top-level success message and target-id fields).
+WRITE_PARAMETER_ORDER: List[str] = [
+ 'set_chargemode',
+ 'chargecurrent',
+ 'minimal_pv_soc',
+ 'minimal_permanent_current',
+ 'max_price_eco',
+ 'chargepoint_lock',
+ 'bat_mode',
+ 'bat_power_reserve',
+ 'instant_charging_limit',
+ 'instant_charging_amount',
+ 'instant_charging_soc',
+ 'pv_charging_limit',
+ 'pv_charging_amount',
+ 'pv_charging_soc',
+ 'vehicle',
+ 'manual_soc',
+ 'set_io_output',
+]
+
+# bat_mode/bat_power_reserve are global settings, not chargepoint-scoped - they
+# deliberately stay out of this set so they skip auto-ID resolution and leave
+# chargepoint_nr null in the write response, instead of failing when no
+# chargepoint is known yet.
+CHARGEPOINT_WRITE_PARAMS = {
+ 'set_chargemode', 'chargecurrent', 'minimal_pv_soc', 'minimal_permanent_current',
+ 'max_price_eco', 'chargepoint_lock',
+ 'instant_charging_limit', 'instant_charging_amount', 'instant_charging_soc',
+ 'pv_charging_limit', 'pv_charging_amount', 'pv_charging_soc', 'vehicle', 'manual_soc',
+}
+
+# Full read whitelist (what counts as a recognized read parameter).
+READ_PARAMETER_ORDER: List[str] = [
+ 'get_chargepoint_all',
+ 'get_chargepoint_voltage_p1', 'get_chargepoint_voltage_p2', 'get_chargepoint_voltage_p3',
+ 'get_chargepoint_voltages',
+ 'get_chargepoint_current_p1', 'get_chargepoint_current_p2', 'get_chargepoint_current_p3',
+ 'get_chargepoint_currents',
+ 'get_chargepoint_power', 'get_chargepoint_powers',
+ 'get_chargepoint_imported', 'get_chargepoint_exported',
+ 'get_chargepoint_daily_imported', 'get_chargepoint_daily_exported',
+ 'get_chargepoint_monthly_exported', 'get_chargepoint_yearly_exported',
+ 'get_chargepoint_frequency', 'get_chargepoint_rfid', 'get_chargepoint_rfid_timestamp',
+ 'get_chargepoint_evse_current',
+ 'get_chargepoint_power_factors',
+ 'get_chargepoint_power_factor_p1', 'get_chargepoint_power_factor_p2', 'get_chargepoint_power_factor_p3',
+ 'get_chargepoint_config_name', 'get_chargepoint_connected_vehicle_name',
+ 'get_chargepoint_charge_template_name', 'get_chargepoint_charge_template_min_current',
+ 'get_chargepoint_instant_charging_current', 'get_chargepoint_pv_charging_min_current',
+ 'get_chargepoint_soc', 'get_chargepoint_state_str', 'get_chargepoint_fault_str',
+ 'get_chargepoint_fault_state', 'get_chargepoint_phases_in_use', 'get_chargepoint_plug_state',
+ 'get_chargepoint_charge_state', 'get_chargepoint_chargemode',
+ 'get_counter',
+ 'get_counter_voltage_p1', 'get_counter_voltage_p2', 'get_counter_voltage_p3', 'get_counter_voltages',
+ 'get_counter_current_p1', 'get_counter_current_p2', 'get_counter_current_p3', 'get_counter_currents',
+ 'get_counter_power', 'get_counter_powers', 'get_counter_power_factors',
+ 'get_counter_imported', 'get_counter_exported',
+ 'get_counter_daily_imported', 'get_counter_daily_exported',
+ 'get_counter_frequency', 'get_counter_fault_str', 'get_counter_fault_state',
+ 'battery', 'get_battery',
+ 'get_battery_power', 'get_battery_soc', 'get_battery_currents',
+ 'get_battery_imported', 'get_battery_exported',
+ 'get_battery_daily_imported', 'get_battery_daily_exported',
+ 'get_battery_fault_str', 'get_battery_fault_state', 'get_battery_power_limit_controllable',
+ 'pv', 'get_pv',
+ 'get_pv_power', 'get_pv_currents', 'get_pv_imported', 'get_pv_exported',
+ 'get_pv_daily_exported', 'get_pv_monthly_exported', 'get_pv_yearly_exported',
+ 'get_pv_fault_str', 'get_pv_fault_state',
+ 'consumer', 'get_consumer', 'get_consumer_all',
+ 'get_consumer_power', 'get_consumer_currents', 'get_consumer_voltages', 'get_consumer_powers',
+ 'get_consumer_imported', 'get_consumer_exported', 'get_consumer_daily_imported',
+ 'get_consumer_phases_in_use', 'get_consumer_charge_state', 'get_consumer_state_str',
+ 'get_consumer_fault_str', 'get_consumer_fault_state', 'get_consumer_usage_type',
+ 'get_io_output_all', 'get_io_output',
+]
+
+COMPLEX_PARAMETERS = {
+ 'get_chargepoint_all', 'get_counter', 'battery', 'get_battery', 'pv', 'get_pv',
+ 'get_chargepoint_voltages', 'get_chargepoint_currents', 'get_chargepoint_powers',
+ 'get_chargepoint_power_factors',
+ 'get_counter_voltages', 'get_counter_currents', 'get_counter_powers', 'get_counter_power_factors',
+ 'get_battery_currents', 'get_pv_currents', 'get_io_output_all',
+ 'get_consumer_all', 'get_consumer', 'consumer',
+ 'get_consumer_currents', 'get_consumer_voltages', 'get_consumer_powers',
+}
+
+TYPE_PUBLIC_NAMES = {'bat': 'battery'}
+SCAN_TYPES = ['chargepoint', 'bat', 'pv', 'counter', 'consumer', 'io']
+
+
+def is_chargepoint_parameter(param: str) -> bool:
+ return param in CHARGEPOINT_WRITE_PARAMS or 'chargepoint' in param
+
+
+def is_io_parameter(param: str) -> bool:
+ return param == 'set_io_output' or param.startswith('get_io_')
+
+
+def get_type_from_parameter(param: str) -> str:
+ if 'chargepoint' in param:
+ return 'chargepoint'
+ if 'io' in param:
+ return 'io'
+ if 'battery' in param:
+ return 'bat'
+ if 'pv' in param:
+ return 'pv'
+ if 'counter' in param:
+ return 'counter'
+ if 'consumer' in param:
+ return 'consumer'
+ return 'chargepoint'
+
+
+def get_success_message(param: str, value: Any, chargepoint_id: Any) -> str:
+ if param == 'set_chargemode':
+ return f"Chargemode for chargepoint {chargepoint_id} set to {value}."
+ if param == 'chargecurrent':
+ return f"Chargecurrent for chargepoint {chargepoint_id} set to {value}A"
+ if param == 'instant_charging_limit':
+ return f"Instant charging limit for chargepoint {chargepoint_id} set to {value}."
+ if param == 'instant_charging_amount':
+ return f"Instant charging amount for chargepoint {chargepoint_id} set to {value}kWh."
+ if param == 'instant_charging_soc':
+ return f"Instant charging SoC for chargepoint {chargepoint_id} set to {value}%."
+ if param == 'pv_charging_limit':
+ return f"PV charging limit for chargepoint {chargepoint_id} set to {value}."
+ if param == 'pv_charging_amount':
+ return f"PV charging amount for chargepoint {chargepoint_id} set to {value}kWh."
+ if param == 'pv_charging_soc':
+ return f"PV charging SoC for chargepoint {chargepoint_id} set to {value}%."
+ if param == 'vehicle':
+ return f"Vehicle {value} assigned to chargepoint {chargepoint_id}."
+ if param == 'manual_soc':
+ return f"Manual SoC set to {value}% for chargepoint {chargepoint_id}."
+ if param == 'set_io_output':
+ return f"IO output set to {value} for io {chargepoint_id}."
+ return f"Parameter {param} set to {value}."
+
+
+def format_raw_output(data: Any, param_name: Optional[str] = None) -> Any:
+ if isinstance(data, dict) and data:
+ first_key = next(iter(data))
+ first_value = data[first_key]
+ if isinstance(first_value, str):
+ return first_value
+ if isinstance(first_value, dict) and len(first_value) == 1:
+ return next(iter(first_value.values()))
+ return first_value
+ return data
+
+
+def _as_float(value: Any, default: float = 0.0) -> float:
+ try:
+ return float(default) if value is None else float(value)
+ except (TypeError, ValueError):
+ return float(default)
+
+
+def _as_int(value: Any, default: int = 0) -> int:
+ try:
+ return int(default) if value is None else int(value)
+ except (TypeError, ValueError):
+ return int(default)
+
+
+def _as_bool(value: Any, default: Any = None) -> bool:
+ # default is unused (bool has no sensible "missing" distinct from False) -
+ # accepted only so this coercer has the same (value, default) signature as
+ # the others and can sit in the same dispatch table.
+ if isinstance(value, bool):
+ return value
+ if isinstance(value, str):
+ return value.strip().lower() in ('true', '1', 'yes', 'on')
+ return bool(value)
+
+
+def _as_str(value: Any, default: Any = '') -> Any:
+ if value is None:
+ return default
+ return value if isinstance(value, str) else str(value)
+
+
+def _phases(value: Any, default: Any = None) -> List[float]:
+ # default is unused (each slot already defaults to 0.0) - accepted only for
+ # a uniform (value, default) coercer signature, see _as_bool.
+ if not isinstance(value, (list, tuple)):
+ return [0.0, 0.0, 0.0]
+ return [_as_float(value[i]) if i < len(value) else 0.0 for i in range(3)]
+
+
+def _is_total(id_: Any) -> bool:
+ return isinstance(id_, str) and id_.strip().lower() == 'total'
+
+
+def _raw(daemon: Any, topic: str) -> Any:
+ return daemon.raw_value_cache.get(topic)
+
+
+def _comp_scalar(component: str, id_: Any, field: str, value: Any) -> Dict[str, Any]:
+ return {f"{component}_{id_}": {field: value}}
+
+
+def _get_chargepoint_all(daemon: Any, id_: Any) -> Dict[str, Any]:
+ prefix = f"openWB/chargepoint/{id_}/get/"
+
+ template = daemon.charge_template_cache.get(str(id_)) or {}
+ chargemode_cfg = template.get('chargemode', {}) or {}
+ chargemode = chargemode_cfg.get('selected', 'stop')
+ instant_charging = chargemode_cfg.get('instant_charging', {}) or {}
+ pv_charging = chargemode_cfg.get('pv_charging', {}) or {}
+ eco_charging = chargemode_cfg.get('eco_charging', {}) or {}
+ scheduled_charging = chargemode_cfg.get('scheduled_charging', {}) or {}
+ instant_limit = instant_charging.get('limit', {}) or {}
+ pv_limit = pv_charging.get('limit', {}) or {}
+
+ instant_charging_current = _as_float(instant_charging.get('current', 0))
+ pv_charging_min_current = _as_float(pv_charging.get('min_current', 0))
+
+ if chargemode == 'instant_charging':
+ min_current = instant_charging_current
+ elif chargemode in ('pv_charging', 'eco_charging'):
+ min_current = pv_charging_min_current
+ elif chargemode == 'scheduled_charging':
+ min_current = _as_float(scheduled_charging.get('current', 0))
+ else:
+ min_current = 0.0
+
+ connected_vehicle_soc = _raw(daemon, prefix + 'connected_vehicle/soc') or {}
+ connected_vehicle_info = _raw(daemon, prefix + 'connected_vehicle/info') or {}
+ config = _raw(daemon, f"openWB/chargepoint/{id_}/config") or {}
+
+ return {
+ f"chargepoint_{id_}": {
+ 'power': _as_float(_raw(daemon, prefix + 'power')),
+ 'voltages': _phases(_raw(daemon, prefix + 'voltages')),
+ 'currents': _phases(_raw(daemon, prefix + 'currents')),
+ 'powers': _phases(_raw(daemon, prefix + 'powers')),
+ 'state_str': _as_str(_raw(daemon, prefix + 'state_str'), 'Unbekannt'),
+ 'fault_str': _as_str(_raw(daemon, prefix + 'fault_str'), 'Kein Fehler'),
+ 'fault_state': _as_int(_raw(daemon, prefix + 'fault_state')),
+ 'imported': _as_float(_raw(daemon, prefix + 'imported')),
+ 'exported': _as_float(_raw(daemon, prefix + 'exported')),
+ 'daily_imported': _as_float(_raw(daemon, prefix + 'daily_imported')),
+ 'daily_exported': _as_float(_raw(daemon, prefix + 'daily_exported')),
+ 'phases_in_use': _as_int(_raw(daemon, prefix + 'phases_in_use'), 1),
+ 'plug_state': _as_bool(_raw(daemon, prefix + 'plug_state')),
+ 'charge_state': _as_bool(_raw(daemon, prefix + 'charge_state')),
+ 'pro_soc': _as_float(_raw(daemon, prefix + 'soc')),
+ 'soc_timestamp': _raw(daemon, prefix + 'soc_timestamp'),
+ 'vehicle_id': _raw(daemon, prefix + 'vehicle_id'),
+ 'evse_current': _as_float(_raw(daemon, prefix + 'evse_current')),
+ 'frequency': _as_float(_raw(daemon, prefix + 'frequency')),
+ 'power_factors': _phases(_raw(daemon, prefix + 'power_factors')),
+ 'rfid': _as_str(_raw(daemon, prefix + 'rfid'), None),
+ 'rfid_timestamp': _raw(daemon, prefix + 'rfid_timestamp'),
+ 'config_name': config.get('name'),
+ 'connected_vehicle_name': connected_vehicle_info.get('name'),
+ 'charge_template_name': template.get('name'),
+ 'min_current': min_current,
+ 'instant_charging_current': instant_charging_current,
+ 'pv_charging_min_current': pv_charging_min_current,
+ 'instant_charging_limit': instant_limit.get('selected', 'none'),
+ 'instant_charging_amount': _as_int(instant_limit.get('amount', 0)),
+ 'instant_charging_soc': _as_int(instant_limit.get('soc', 0)),
+ 'pv_charging_limit': pv_limit.get('selected', 'none'),
+ 'pv_charging_amount': _as_int(pv_limit.get('amount', 0)),
+ 'pv_charging_soc': _as_int(pv_limit.get('soc', 0)),
+ 'max_price_eco': _as_float(eco_charging.get('max_price', 0)) * 100000,
+ 'soc': _as_float(connected_vehicle_soc.get('soc', 0)),
+ 'range_charged': _as_float(connected_vehicle_soc.get('range_charged', 0)),
+ 'chargemode': chargemode,
+ 'manual_lock': _as_bool(_raw(daemon, f"openWB/chargepoint/{id_}/set/manual_lock")),
+ }
+ }
+
+
+def _make_scalar_handler(
+ component: str, mqtt_component: str, suffix: str, field: str,
+ coercer: Callable[[Any, Any], Any], default: Any, total_aware: bool) -> Callable[[Any, Any], Dict[str, Any]]:
+ """Build a read handler for the common "look up one get/ topic, coerce
+ it, wrap it as {component_id: {field: value}}" shape - optionally serving
+ id="total" from daemon.total_sums instead, for the handful of metrics that are
+ continuously aggregated across all known IDs of a component type."""
+ def handler(daemon: Any, id_: Any) -> Dict[str, Any]:
+ if total_aware and _is_total(id_):
+ return {f"{component}_total": {field: daemon.total_sums[mqtt_component][field]}}
+ raw = _raw(daemon, f"openWB/{mqtt_component}/{id_}/get/{suffix}")
+ return _comp_scalar(component, id_, field, coercer(raw, default))
+ return handler
+
+
+# Scalar read-parameter table: one row per `get__` handler that
+# reduces to "read openWB///get/, coerce, wrap".
+# total_aware marks the metrics that are also continuously aggregated across all
+# known IDs and so additionally serve id="total" from daemon.total_sums.
+#
+# (component, mqtt_component, topic_suffix, field, coercer, default, total_aware)
+SCALAR_READ_TABLE: List[Tuple[str, str, str, str, Callable[[Any, Any], Any], Any, bool]] = [
+ # chargepoint
+ ('chargepoint', 'chargepoint', 'power', 'power', _as_float, 0.0, True),
+ ('chargepoint', 'chargepoint', 'powers', 'powers', _phases, None, False),
+ ('chargepoint', 'chargepoint', 'imported', 'imported', _as_float, 0.0, True),
+ ('chargepoint', 'chargepoint', 'exported', 'exported', _as_float, 0.0, True),
+ ('chargepoint', 'chargepoint', 'daily_imported', 'daily_imported', _as_float, 0.0, False),
+ ('chargepoint', 'chargepoint', 'daily_exported', 'daily_exported', _as_float, 0.0, False),
+ ('chargepoint', 'chargepoint', 'monthly_exported', 'monthly_exported', _as_float, 0.0, True),
+ ('chargepoint', 'chargepoint', 'yearly_exported', 'yearly_exported', _as_float, 0.0, True),
+ ('chargepoint', 'chargepoint', 'frequency', 'frequency', _as_float, 0.0, False),
+ ('chargepoint', 'chargepoint', 'rfid', 'rfid', _as_str, '', False),
+ ('chargepoint', 'chargepoint', 'rfid_timestamp', 'rfid_timestamp', _as_int, 0, False),
+ ('chargepoint', 'chargepoint', 'evse_current', 'evse_current', _as_float, 0.0, False),
+ ('chargepoint', 'chargepoint', 'soc', 'soc', _as_int, 0, False),
+ ('chargepoint', 'chargepoint', 'state_str', 'state_str', _as_str, '', False),
+ ('chargepoint', 'chargepoint', 'fault_str', 'fault_str', _as_str, '', False),
+ ('chargepoint', 'chargepoint', 'fault_state', 'fault_state', _as_int, 0, False),
+ ('chargepoint', 'chargepoint', 'phases_in_use', 'phases_in_use', _as_int, 0, False),
+ ('chargepoint', 'chargepoint', 'plug_state', 'plug_state', _as_bool, None, False),
+ ('chargepoint', 'chargepoint', 'charge_state', 'charge_state', _as_bool, None, False),
+ ('chargepoint', 'chargepoint', 'chargemode', 'chargemode', _as_str, '', False),
+ # counter
+ ('counter', 'counter', 'power', 'power', _as_float, 0.0, False),
+ ('counter', 'counter', 'powers', 'powers', _phases, None, False),
+ ('counter', 'counter', 'power_factors', 'power_factors', _phases, None, False),
+ ('counter', 'counter', 'imported', 'imported', _as_float, 0.0, False),
+ ('counter', 'counter', 'exported', 'exported', _as_float, 0.0, False),
+ ('counter', 'counter', 'daily_imported', 'daily_imported', _as_float, 0.0, False),
+ ('counter', 'counter', 'daily_exported', 'daily_exported', _as_float, 0.0, False),
+ ('counter', 'counter', 'frequency', 'frequency', _as_float, 50.0, False),
+ ('counter', 'counter', 'fault_str', 'fault_str', _as_str, 'Kein Fehler', False),
+ ('counter', 'counter', 'fault_state', 'fault_state', _as_int, 0, False),
+ # battery (mqtt topic path is "bat", output/field prefix is "battery")
+ ('battery', 'bat', 'power', 'power', _as_float, 0.0, False),
+ ('battery', 'bat', 'soc', 'soc', _as_int, 0, False),
+ ('battery', 'bat', 'currents', 'currents', _phases, None, False),
+ ('battery', 'bat', 'imported', 'imported', _as_float, 0.0, False),
+ ('battery', 'bat', 'exported', 'exported', _as_float, 0.0, False),
+ ('battery', 'bat', 'daily_imported', 'daily_imported', _as_float, 0.0, False),
+ ('battery', 'bat', 'daily_exported', 'daily_exported', _as_float, 0.0, False),
+ ('battery', 'bat', 'fault_str', 'fault_str', _as_str, 'Kein Fehler', False),
+ ('battery', 'bat', 'fault_state', 'fault_state', _as_int, 0, False),
+ ('battery', 'bat', 'power_limit_controllable', 'power_limit_controllable', _as_bool, None, False),
+ # pv
+ ('pv', 'pv', 'power', 'power', _as_float, 0.0, True),
+ ('pv', 'pv', 'currents', 'currents', _phases, None, False),
+ ('pv', 'pv', 'imported', 'imported', _as_float, 0.0, True),
+ ('pv', 'pv', 'exported', 'exported', _as_float, 0.0, True),
+ ('pv', 'pv', 'daily_exported', 'daily_exported', _as_float, 0.0, False),
+ ('pv', 'pv', 'monthly_exported', 'monthly_exported', _as_float, 0.0, True),
+ ('pv', 'pv', 'yearly_exported', 'yearly_exported', _as_float, 0.0, True),
+ ('pv', 'pv', 'fault_str', 'fault_str', _as_str, 'Kein Fehler', False),
+ ('pv', 'pv', 'fault_state', 'fault_state', _as_int, 0, False),
+ # consumer
+ ('consumer', 'consumer', 'power', 'power', _as_float, 0.0, False),
+ ('consumer', 'consumer', 'currents', 'currents', _phases, None, False),
+ ('consumer', 'consumer', 'voltages', 'voltages', _phases, None, False),
+ ('consumer', 'consumer', 'powers', 'powers', _phases, None, False),
+ ('consumer', 'consumer', 'imported', 'imported', _as_float, 0.0, False),
+ ('consumer', 'consumer', 'exported', 'exported', _as_float, 0.0, False),
+ ('consumer', 'consumer', 'daily_imported', 'daily_imported', _as_float, 0.0, False),
+ ('consumer', 'consumer', 'phases_in_use', 'phases_in_use', _as_int, 0, False),
+ ('consumer', 'consumer', 'charge_state', 'charge_state', _as_bool, None, False),
+ ('consumer', 'consumer', 'state_str', 'state_str', _as_str, None, False),
+ ('consumer', 'consumer', 'fault_str', 'fault_str', _as_str, 'Kein Fehler', False),
+ ('consumer', 'consumer', 'fault_state', 'fault_state', _as_int, 0, False),
+]
+
+
+def _make_phase_handlers(component: str, mqtt_component: str, suffix: str) -> Dict[str, Callable[..., Any]]:
+ """Build the 4 read handlers (plural array + 3 per-phase scalars) that share
+ one underlying openWB///get/ topic, e.g. suffix
+ "voltages" yields get__voltages plus _voltage_p1/_p2/_p3."""
+ singular = suffix[:-1] # 'voltages' -> 'voltage', 'power_factors' -> 'power_factor'
+
+ def topic(id_: Any) -> str:
+ return f"openWB/{mqtt_component}/{id_}/get/{suffix}"
+
+ def plural_handler(daemon: Any, id_: Any) -> Dict[str, Any]:
+ return _comp_scalar(component, id_, suffix, _phases(_raw(daemon, topic(id_))))
+
+ def make_phase_handler(phase: int) -> Callable[[Any, Any], Dict[str, Any]]:
+ def handler(daemon: Any, id_: Any) -> Dict[str, Any]:
+ values = _phases(_raw(daemon, topic(id_)))
+ return _comp_scalar(component, id_, f"{singular}_p{phase}", values[phase - 1])
+ return handler
+
+ handlers: Dict[str, Callable[..., Any]] = {f"get_{component}_{suffix}": plural_handler}
+ for phase in (1, 2, 3):
+ handlers[f"get_{component}_{singular}_p{phase}"] = make_phase_handler(phase)
+ return handlers
+
+
+# (component, mqtt_component, topic_suffix) for the phase-indexed getters that
+# have both a plural array endpoint and 3 individual per-phase endpoints.
+PHASE_READ_TABLE: List[Tuple[str, str, str]] = [
+ ('chargepoint', 'chargepoint', 'voltages'),
+ ('chargepoint', 'chargepoint', 'currents'),
+ ('chargepoint', 'chargepoint', 'power_factors'),
+ ('counter', 'counter', 'voltages'),
+ ('counter', 'counter', 'currents'),
+]
+
+
+def _get_chargepoint_config_name(daemon, id_):
+ config = _raw(daemon, f"openWB/chargepoint/{id_}/config") or {}
+ return _comp_scalar('chargepoint', id_, 'config_name', _as_str(config.get('name'), ''))
+
+
+def _get_chargepoint_connected_vehicle_name(daemon, id_):
+ info = _raw(daemon, f"openWB/chargepoint/{id_}/get/connected_vehicle/info") or {}
+ return _comp_scalar('chargepoint', id_, 'connected_vehicle_name', _as_str(info.get('name'), ''))
+
+
+def _get_chargepoint_charge_template_name(daemon, id_):
+ template = daemon.charge_template_cache.get(str(id_)) or {}
+ return _comp_scalar('chargepoint', id_, 'charge_template_name', _as_str(template.get('name'), ''))
+
+
+def _get_chargepoint_charge_template_min_current(daemon, id_):
+ template = daemon.charge_template_cache.get(str(id_)) or {}
+ min_current = template.get('chargemode', {}).get('pv_charging', {}).get('min_current', 0)
+ return _comp_scalar('chargepoint', id_, 'charge_template_min_current', _as_float(min_current))
+
+
+def _get_chargepoint_instant_charging_current(daemon, id_):
+ template = daemon.charge_template_cache.get(str(id_)) or {}
+ current = template.get('chargemode', {}).get('instant_charging', {}).get('current', 0)
+ return _comp_scalar('chargepoint', id_, 'instant_charging_current', _as_float(current))
+
+
+def _get_chargepoint_pv_charging_min_current(daemon, id_):
+ template = daemon.charge_template_cache.get(str(id_)) or {}
+ min_current = template.get('chargemode', {}).get('pv_charging', {}).get('min_current', 0)
+ return _comp_scalar('chargepoint', id_, 'pv_charging_min_current', _as_float(min_current))
+
+
+def _get_counter(daemon, id_):
+ prefix = f"openWB/counter/{id_}/get/"
+ return {
+ f"counter_{id_}": {
+ 'power': _as_float(_raw(daemon, prefix + 'power')),
+ 'voltages': _phases(_raw(daemon, prefix + 'voltages')),
+ 'currents': _phases(_raw(daemon, prefix + 'currents')),
+ 'powers': _phases(_raw(daemon, prefix + 'powers')),
+ 'power_factors': _phases(_raw(daemon, prefix + 'power_factors')),
+ 'frequency': _as_float(_raw(daemon, prefix + 'frequency'), 50.0),
+ 'exported': _as_float(_raw(daemon, prefix + 'exported')),
+ 'daily_exported': _as_float(_raw(daemon, prefix + 'daily_exported')),
+ 'imported': _as_float(_raw(daemon, prefix + 'imported')),
+ 'daily_imported': _as_float(_raw(daemon, prefix + 'daily_imported')),
+ 'fault_str': _as_str(_raw(daemon, prefix + 'fault_str'), 'Kein Fehler'),
+ 'fault_state': _as_int(_raw(daemon, prefix + 'fault_state')),
+ }
+ }
+
+
+def _get_battery(daemon, id_):
+ prefix = f"openWB/bat/{id_}/get/"
+ return {
+ f"battery_{id_}": {
+ 'power': _as_float(_raw(daemon, prefix + 'power')),
+ 'soc': _as_int(_raw(daemon, prefix + 'soc')),
+ 'currents': _phases(_raw(daemon, prefix + 'currents')),
+ 'imported': _as_float(_raw(daemon, prefix + 'imported')),
+ 'exported': _as_float(_raw(daemon, prefix + 'exported')),
+ 'daily_imported': _as_float(_raw(daemon, prefix + 'daily_imported')),
+ 'daily_exported': _as_float(_raw(daemon, prefix + 'daily_exported')),
+ 'fault_str': _as_str(_raw(daemon, prefix + 'fault_str'), 'Kein Fehler'),
+ 'fault_state': _as_int(_raw(daemon, prefix + 'fault_state')),
+ 'power_limit_controllable': _as_bool(_raw(daemon, prefix + 'power_limit_controllable')),
+ }
+ }
+
+
+def _get_pv(daemon, id_):
+ prefix = f"openWB/pv/{id_}/get/"
+ return {
+ f"pv_{id_}": {
+ 'power': _as_float(_raw(daemon, prefix + 'power')),
+ 'currents': _phases(_raw(daemon, prefix + 'currents')),
+ 'exported': _as_float(_raw(daemon, prefix + 'exported')),
+ 'daily_exported': _as_float(_raw(daemon, prefix + 'daily_exported')),
+ 'monthly_exported': _as_float(_raw(daemon, prefix + 'monthly_exported')),
+ 'yearly_exported': _as_float(_raw(daemon, prefix + 'yearly_exported')),
+ 'fault_str': _as_str(_raw(daemon, prefix + 'fault_str'), 'Kein Fehler'),
+ 'fault_state': _as_int(_raw(daemon, prefix + 'fault_state')),
+ }
+ }
+
+
+def _get_consumer_all(daemon, id_):
+ prefix = f"openWB/consumer/{id_}/get/"
+ usage = _raw(daemon, f"openWB/consumer/{id_}/usage")
+ usage_type = usage.get('type') if isinstance(usage, dict) else None
+ return {
+ f"consumer_{id_}": {
+ 'usage_type': usage_type,
+ 'power': _as_float(_raw(daemon, prefix + 'power')),
+ 'currents': _phases(_raw(daemon, prefix + 'currents')),
+ 'voltages': _phases(_raw(daemon, prefix + 'voltages')),
+ 'powers': _phases(_raw(daemon, prefix + 'powers')),
+ 'imported': _as_float(_raw(daemon, prefix + 'imported')),
+ 'exported': _as_float(_raw(daemon, prefix + 'exported')),
+ 'daily_imported': _as_float(_raw(daemon, prefix + 'daily_imported')),
+ 'phases_in_use': _as_int(_raw(daemon, prefix + 'phases_in_use')),
+ 'charge_state': _as_bool(_raw(daemon, prefix + 'charge_state')),
+ 'state_str': _as_str(_raw(daemon, prefix + 'state_str'), None),
+ 'fault_str': _as_str(_raw(daemon, prefix + 'fault_str'), 'Kein Fehler'),
+ 'fault_state': _as_int(_raw(daemon, prefix + 'fault_state')),
+ }
+ }
+
+
+def _get_consumer_usage_type(daemon, id_):
+ usage = _raw(daemon, f"openWB/consumer/{id_}/usage")
+ usage_type = usage.get('type') if isinstance(usage, dict) else None
+ return _comp_scalar('consumer', id_, 'usage_type', usage_type)
+
+
+def _get_io_output_all(daemon, id_):
+ return {
+ f"io_{id_}": {
+ 'digital_output': daemon._get_io_output_map(id_, 'digital_output'),
+ 'analog_output': daemon._get_io_output_map(id_, 'analog_output'),
+ }
+ }
+
+
+def _get_io_output(daemon, id_, options):
+ output_name = options.get('io_output') if options else None
+ if output_name is None or str(output_name).strip() == '':
+ return _get_io_output_all(daemon, id_)
+
+ output_name = str(output_name).strip()
+ output_type = daemon._normalize_io_output_type(options.get('io_output_type'))
+ result = None
+
+ if output_type == 'digital_output' or output_type is None:
+ digital = daemon._get_io_output_map(id_, 'digital_output')
+ if output_name in digital:
+ result = digital[output_name]
+ output_type = 'digital_output'
+
+ if result is None and (output_type == 'analog_output' or output_type is None):
+ analog = daemon._get_io_output_map(id_, 'analog_output')
+ if output_name in analog:
+ result = analog[output_name]
+ output_type = 'analog_output'
+
+ return {f"io_{id_}": {'output': output_name, 'output_type': output_type, 'value': result}}
+
+
+def _build_scalar_read_dispatch() -> Dict[str, Callable[..., Dict[str, Any]]]:
+ dispatch: Dict[str, Callable[..., Dict[str, Any]]] = {}
+ for component, mqtt_component, suffix, field, coercer, default, total_aware in SCALAR_READ_TABLE:
+ dispatch[f"get_{component}_{field}"] = _make_scalar_handler(
+ component, mqtt_component, suffix, field, coercer, default, total_aware)
+ for component, mqtt_component, suffix in PHASE_READ_TABLE:
+ dispatch.update(_make_phase_handlers(component, mqtt_component, suffix))
+ return dispatch
+
+
+READ_DISPATCH: Dict[str, Callable[..., Dict[str, Any]]] = {
+ 'get_chargepoint_all': _get_chargepoint_all,
+ **_build_scalar_read_dispatch(),
+ 'get_chargepoint_config_name': _get_chargepoint_config_name,
+ 'get_chargepoint_connected_vehicle_name': _get_chargepoint_connected_vehicle_name,
+ 'get_chargepoint_charge_template_name': _get_chargepoint_charge_template_name,
+ 'get_chargepoint_charge_template_min_current': _get_chargepoint_charge_template_min_current,
+ 'get_chargepoint_instant_charging_current': _get_chargepoint_instant_charging_current,
+ 'get_chargepoint_pv_charging_min_current': _get_chargepoint_pv_charging_min_current,
+ 'get_counter': _get_counter,
+ 'battery': _get_battery,
+ 'get_battery': _get_battery,
+ 'pv': _get_pv,
+ 'get_pv': _get_pv,
+ 'consumer': _get_consumer_all,
+ 'get_consumer': _get_consumer_all,
+ 'get_consumer_all': _get_consumer_all,
+ 'get_consumer_usage_type': _get_consumer_usage_type,
+ 'get_io_output_all': _get_io_output_all,
+ 'get_io_output': _get_io_output,
+}
+
+# Read handlers that need the request's io_output/io_output_type options in
+# addition to (daemon, id) - every other read handler ignores options entirely.
+READ_PARAMS_WITH_OPTIONS = {'get_io_output'}
+
+WRITE_DISPATCH: Dict[str, Callable[..., Tuple[bool, str]]] = {
+ 'set_chargemode': lambda d, v, i, o: d._set_chargemode(i, v),
+ 'chargecurrent': lambda d, v, i, o: d._set_chargecurrent(i, v),
+ 'minimal_pv_soc': lambda d, v, i, o: d._set_minimal_pv_soc(i, v),
+ 'minimal_permanent_current': lambda d, v, i, o: d._set_minimal_permanent_current(i, v),
+ 'max_price_eco': lambda d, v, i, o: d._set_max_price_eco(i, v),
+ 'chargepoint_lock': lambda d, v, i, o: d._set_chargepoint_lock(i, v),
+ 'bat_mode': lambda d, v, i, o: d._handle_bat_mode_operation(v),
+ 'bat_power_reserve': lambda d, v, i, o: d._handle_bat_power_reserve_operation(v),
+ 'instant_charging_limit': lambda d, v, i, o: d._handle_instant_charging_limit_operation(v, i),
+ 'instant_charging_amount': lambda d, v, i, o: d._handle_instant_charging_limit_amount_operation(v, i),
+ 'instant_charging_soc': lambda d, v, i, o: d._handle_instant_charging_limit_soc_operation(v, i),
+ 'pv_charging_limit': lambda d, v, i, o: d._handle_pv_charging_limit_operation(v, i),
+ 'pv_charging_amount': lambda d, v, i, o: d._handle_pv_charging_limit_amount_operation(v, i),
+ 'pv_charging_soc': lambda d, v, i, o: d._handle_pv_charging_limit_soc_operation(v, i),
+ 'vehicle': lambda d, v, i, o: d._set_vehicle(i, v),
+ 'manual_soc': lambda d, v, i, o: d._set_manual_soc(i, v),
+ 'set_io_output': lambda d, v, i, o: d._set_io_output(i, v, o.get('io_output'), o.get('io_output_type')),
+}
+
+
+def list_components(daemon: Any) -> Dict[str, List[int]]:
+ return {
+ TYPE_PUBLIC_NAMES.get(type_, type_): sorted(daemon.all_ids.get(type_, set()))
+ for type_ in SCAN_TYPES
+ }
+
+
+class ParameterHandler:
+ def __init__(self, daemon: Any):
+ self.daemon = daemon
+
+ def read_parameter(
+ self, param: str, id_: Any, options: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
+ handler = READ_DISPATCH.get(param)
+ if handler is None:
+ return None
+ if param in READ_PARAMS_WITH_OPTIONS:
+ return handler(self.daemon, id_, options or {})
+ return handler(self.daemon, id_)
+
+ def write_parameter(
+ self, param: str, value: Any, target_id: Any = None,
+ options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
+ handler = WRITE_DISPATCH.get(param)
+ if handler is None:
+ return {'success': False, 'message': 'Unknown write parameter'}
+ try:
+ success, message = handler(self.daemon, value, target_id, options or {})
+ return {'success': success, 'message': message}
+ except Exception as exc:
+ return {'success': False, 'message': str(exc)}
+
+ def list_components(self) -> Dict[str, List[int]]:
+ return list_components(self.daemon)
+
+
+def extract_write_params(all_params: Dict[str, Any]) -> Dict[str, Any]:
+ return {k: all_params[k] for k in WRITE_PARAMETER_ORDER if k in all_params}
+
+
+def extract_read_params(all_params: Dict[str, Any]) -> Dict[str, Any]:
+ return {k: all_params[k] for k in READ_PARAMETER_ORDER if k in all_params}
+
+
+def _io_options(all_params: Dict[str, Any]) -> Dict[str, Any]:
+ return {'io_output': all_params.get('io_output'), 'io_output_type': all_params.get('io_output_type')}
+
+
+def dispatch_write(ph: ParameterHandler, write_params: Dict[str, Any], all_params: Dict[str, Any]) -> Dict[str, Any]:
+ daemon = ph.daemon
+ first_target_id = None
+
+ for param, value in write_params.items():
+ target_id = None
+ id_type = None
+ if is_chargepoint_parameter(param):
+ target_id = all_params.get('chargepoint_nr')
+ id_type = 'chargepoint'
+ elif is_io_parameter(param):
+ target_id = all_params.get('io_nr')
+ id_type = 'io'
+
+ if id_type is not None and target_id in (None, '', 'auto'):
+ resolved = daemon.lowest_ids.get(id_type)
+ if resolved is None:
+ return {'success': False, 'message': f"No {id_type} devices available for auto-ID"}
+ target_id = resolved
+
+ result = ph.write_parameter(param, value, target_id, _io_options(all_params))
+
+ if first_target_id is None:
+ first_target_id = target_id
+ if not result['success']:
+ return result
+
+ first_param = next(iter(write_params))
+ first_value = write_params[first_param]
+ return {
+ 'success': True,
+ 'message': get_success_message(first_param, first_value, first_target_id),
+ 'data': {
+ 'chargepoint_nr': first_target_id if is_chargepoint_parameter(first_param) else None,
+ 'io_nr': first_target_id if is_io_parameter(first_param) else None,
+ first_param: first_value,
+ },
+ }
+
+
+def dispatch_read(
+ ph: ParameterHandler, read_params: Dict[str, Any], all_params: Dict[str, Any],
+ debug: bool) -> Tuple[Dict[str, Any], List[str]]:
+ result: Dict[str, Any] = {}
+ debug_info: List[str] = []
+ options = _io_options(all_params)
+
+ for param, id_ in read_params.items():
+ resolved_id = id_
+ if id_ in ('auto', ''):
+ type_ = get_type_from_parameter(param)
+ resolved_id = ph.daemon.lowest_ids.get(type_)
+ if resolved_id is None:
+ if debug:
+ debug_info.append(f"Auto-ID failed for {param}: no {type_} devices known")
+ if type_ == 'chargepoint':
+ resolved_id = 0
+ else:
+ continue
+
+ data = ph.read_parameter(param, resolved_id, options)
+ if data is not None:
+ result.update(data)
+
+ return result, debug_info