diff --git a/.editorconfig b/.editorconfig index fc6b64283e..9b49f25103 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,6 +6,10 @@ charset = utf-8 end_of_line = lf insert_final_newline = true +[simpleAPI/**.php] +indent_style = space +indent_size = 4 + [*.{py,yml,lock}] indent_style = space indent_size = 4 diff --git a/simpleAPI/config/config.php b/simpleAPI/config/config.php index e60fef968b..0dddb3a5f6 100644 --- a/simpleAPI/config/config.php +++ b/simpleAPI/config/config.php @@ -20,12 +20,12 @@ 'auth' => [ 'enabled' => false, 'require_https' => false, - + // Gültige Tokens 'tokens' => [ // 'your-secret-token-here' ], - + // Benutzer (Username => Passwort/Hash) 'users' => [ // 'admin' => password_hash('admin123', PASSWORD_DEFAULT), diff --git a/simpleAPI/simpleAPI_mqtt.py b/simpleAPI/simpleAPI_mqtt.py index c6a637fb64..e41235694e 100755 --- a/simpleAPI/simpleAPI_mqtt.py +++ b/simpleAPI/simpleAPI_mqtt.py @@ -166,14 +166,17 @@ def _transform_and_publish(self, original_topic: str, payload: str): """Transform original topic to simpleAPI format and publish if value changed.""" try: log.debug(f"DEBUG: Processing original topic: {original_topic}") - + # Parse the payload 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: {list(transformed_topics.keys())}") + + 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(): @@ -250,9 +253,9 @@ 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}") - + # FIRST: Handle connected_vehicle transformations (both /get/ and direct) # This must happen BEFORE any other filtering if '/connected_vehicle/config/chargemode' in simple_base: @@ -263,45 +266,45 @@ def _transform_chargepoint_topic(self, simple_base: str) -> Optional[str]: 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: Chargemode transformation successful: {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) - + # Keep only config topics that are in the allowed list if '/config/' in simple_base and not re.search(r'/(chargemode|vehicle_name)$', simple_base): allowed_config_paths = [ - 'configuration/ip_address', 'configuration/duo_num', 'ev', 'name', - 'type', 'template', 'connected_phases', 'phase_1', + 'configuration/ip_address', 'configuration/duo_num', 'ev', 'name', + 'type', 'template', 'connected_phases', 'phase_1', 'auto_phase_switch_hw', 'control_pilot_interruption_hw', 'id', 'ocpp_chargebox_id' ] - + # Extract the config path part config_match = re.search(r'/config/(.+)$', simple_base) if config_match: config_path = config_match.group(1) if config_path in allowed_config_paths: return simple_base # Keep this config topic - + return None # Filter out other config topics - + # Handle set topics with special mappings if '/set/' in simple_base: # set/manual_lock -> manual_lock simple_base = re.sub(r'/set/manual_lock$', '/manual_lock', simple_base) - + # set/current -> evse_current simple_base = re.sub(r'/set/current$', '/evse_current', simple_base) - + # Filter out all other set topics (like charge_template, log, etc.) if '/set/' in simple_base: return None - + # Handle get topics - remove /get/ prefix if '/get/' in simple_base: simple_base = simple_base.replace('/get/', '/') - + # Filter out unwanted topics - but exclude already transformed ones if not re.search(r'/(chargemode|vehicle_name)$', simple_base): unwanted_patterns = [ @@ -310,7 +313,7 @@ def _transform_chargepoint_topic(self, simple_base: str) -> Optional[str]: r'/current_branch$', r'/current_commit$' ] - + for pattern in unwanted_patterns: if re.search(pattern, simple_base): return None @@ -321,7 +324,7 @@ def _transform_chargepoint_topic(self, simple_base: str) -> Optional[str]: if re.search(r'/connected_vehicle/', simple_base): log.debug(f"DEBUG: Filtering out connected_vehicle topic: {simple_base}") return None - + log.debug(f"DEBUG: _transform_chargepoint_topic output: {simple_base}") return simple_base @@ -361,7 +364,7 @@ def _expand_value_to_topics(self, base_topic: str, value: Any, result: Dict[str, final_topic = transformed else: return # Topic should be filtered out - + result[final_topic] = value def _generate_simplified_topics(self, simple_topic: str, parsed_value: Any) -> Dict[str, Any]: @@ -380,13 +383,13 @@ def _generate_simplified_topics(self, simple_topic: str, parsed_value: Any) -> D # Check if this is the lowest ID for this component type if component_type in self.lowest_ids and self.lowest_ids[component_type] == component_id: simplified_base = f"openWB/simpleAPI/{component_type}/{remaining_path}" - + # Apply chargepoint transformations to simplified topics as well if component_type == 'chargepoint': simplified_base = self._transform_chargepoint_topic(simplified_base) if simplified_base is None: return result - + self._expand_value_to_topics(simplified_base, parsed_value, result) return result @@ -434,12 +437,12 @@ def _handle_write_operation(self, topic: str, payload: str): if not payload or payload.strip() == "": log.debug(f"Skipping empty set topic: {topic}") return - + log.info(f"Write operation: {topic} = {payload}") # 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) @@ -456,9 +459,9 @@ def _handle_write_operation(self, topic: str, payload: str): if success: self._clear_set_topic(topic) return - + topic_parts = topic_remainder.split('/') - + if len(topic_parts) < 1: log.error(f"Invalid set topic format: {topic}") return @@ -506,7 +509,7 @@ def _handle_chargepoint_operation(self, topic_parts: list, payload: str) -> bool log.error(f"Invalid chargepoint topic format: {'/'.join(topic_parts)}") return False - log.debug(f"Chargepoint operation: ID={chargepoint_id}, parameter={parameter}, value={payload}") + log.debug(f"Charge point operation: ID={chargepoint_id}, parameter={parameter}, value={payload}") if parameter == 'chargemode': self._set_chargemode(chargepoint_id, payload) @@ -523,7 +526,7 @@ def _handle_chargepoint_operation(self, topic_parts: list, payload: str) -> bool else: log.error(f"Unknown chargepoint parameter: {parameter}") return False - + return True def _get_charge_template(self, chargepoint_id: str) -> Optional[Dict[str, Any]]: @@ -664,26 +667,26 @@ def _handle_bat_mode_operation(self, payload: str) -> bool: 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 - + # 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 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 - + # Publish the modified template target_topic = f"openWB/set/chargepoint/{chargepoint_id}/set/charge_template" self._publish_json(target_topic, charge_template) @@ -700,22 +703,22 @@ def _handle_instant_charging_limit_soc_operation(self, payload: str) -> bool: 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) @@ -729,32 +732,35 @@ def _handle_instant_charging_limit_amount_operation(self, payload: str) -> bool: 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 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 ({internal_amount} Wh) for chargepoint {chargepoint_id}") + log.info( + f"Set instant_charging_limit_amount to {amount_value} kWh " + f"({internal_amount} Wh) for chargepoint {chargepoint_id}" + ) return True def _publish_json(self, topic: str, data: Dict[str, Any]): diff --git a/simpleAPI/simpleapi.php b/simpleAPI/simpleapi.php index 5998d60679..fe75951319 100644 --- a/simpleAPI/simpleapi.php +++ b/simpleAPI/simpleapi.php @@ -24,13 +24,13 @@ public function __construct() { // Konfiguration laden $this->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 $this->authenticator = new Authenticator($this->config); } @@ -43,7 +43,7 @@ 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: *'); @@ -59,7 +59,7 @@ public function handleRequest() // Parameter sammeln (GET und POST) $params = array_merge($_GET, $_POST); - + // Debug-Modus if (isset($params['debug']) && $params['debug'] === 'true') { $this->config['debug'] = true; @@ -79,7 +79,7 @@ public function handleRequest() $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']) { @@ -97,7 +97,7 @@ public function handleRequest() $readParams = $this->getReadParameters($params); if (!empty($readParams)) { $result = $this->handleReadRequest($readParams, $params); - + // Raw-Ausgabe Validierung if (isset($params['raw']) && $params['raw'] === 'true') { if (count($readParams) > 1) { @@ -110,7 +110,7 @@ public function handleRequest() ]); return; } - + // Prüfe ob der Parameter für Raw-Output geeignet ist $paramName = array_keys($readParams)[0]; if ($this->isComplexParameter($paramName)) { @@ -123,7 +123,7 @@ public function handleRequest() ]); return; } - + // Einzelner Parameter: Raw-Output verwenden echo $this->formatRawOutput($result); } else { @@ -138,7 +138,6 @@ public function handleRequest() 'success' => false, 'message' => 'No valid parameters provided' ]); - } catch (Exception $e) { http_response_code(500); echo json_encode([ @@ -156,9 +155,13 @@ private function getWriteParameters($params) { $writeParams = []; $writeableKeys = [ - 'set_chargemode', 'chargecurrent', 'minimal_pv_soc', - 'minimal_permanent_current', 'max_price_eco', - 'chargepoint_lock', 'bat_mode' + 'set_chargemode', + 'chargecurrent', + 'minimal_pv_soc', + 'minimal_permanent_current', + 'max_price_eco', + 'chargepoint_lock', + 'bat_mode' ]; foreach ($writeableKeys as $key) { @@ -180,37 +183,79 @@ private function getReadParameters($params) // Chargepoint - Alle Daten 'get_chargepoint_all', // Chargepoint - Spannungen - 'get_chargepoint_voltage_p1', 'get_chargepoint_voltage_p2', 'get_chargepoint_voltage_p3', 'get_chargepoint_voltages', + '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', + 'get_chargepoint_current_p1', + 'get_chargepoint_current_p2', + 'get_chargepoint_current_p3', + 'get_chargepoint_currents', // Chargepoint - Leistungen - 'get_chargepoint_power', 'get_chargepoint_powers', + 'get_chargepoint_power', + 'get_chargepoint_powers', // Chargepoint - Status & Energie - 'get_chargepoint_imported', 'get_chargepoint_exported', '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_chargepoint_imported', + 'get_chargepoint_exported', + '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', + 'get_counter', // Counter - Spannungen - 'get_counter_voltage_p1', 'get_counter_voltage_p2', 'get_counter_voltage_p3', 'get_counter_voltages', + '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', + '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', + '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', + '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', + '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', + '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', + 'get_pv', // PV - Einzelwerte - 'get_pv_power', 'get_pv_currents', 'get_pv_exported', 'get_pv_daily_exported', - 'get_pv_monthly_exported', 'get_pv_yearly_exported', 'get_pv_fault_str', 'get_pv_fault_state' + 'get_pv_power', + 'get_pv_currents', + 'get_pv_exported', + 'get_pv_daily_exported', + 'get_pv_monthly_exported', + 'get_pv_yearly_exported', + 'get_pv_fault_str', + 'get_pv_fault_state' ]; foreach ($readableKeys as $key) { @@ -229,7 +274,7 @@ private function handleWriteRequest($writeParams, $allParams) { foreach ($writeParams as $param => $value) { $chargepointId = $allParams['chargepoint_nr'] ?? null; - + // Auto-ID Feature: Niedrigste ID finden wenn keine angegeben if ($chargepointId === null && $this->isChargepointParameter($param)) { try { @@ -249,7 +294,7 @@ private function handleWriteRequest($writeParams, $allParams) } $result = $this->parameterHandler->writeParameter($param, $value, $chargepointId); - + if (!$result['success']) { return $result; } @@ -258,7 +303,7 @@ private function handleWriteRequest($writeParams, $allParams) // 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, $chargepointId ?? null), @@ -282,7 +327,7 @@ private function handleReadRequest($readParams, $allParams) if ($id === 'auto' || $id === '') { $type = $this->getTypeFromParameter($param); $id = $this->mqttClient->getLowestId($type); - + if ($id === null) { continue; // Skip wenn keine ID gefunden } @@ -297,7 +342,7 @@ private function handleReadRequest($readParams, $allParams) 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 { @@ -331,7 +376,7 @@ private function getTypeFromParameter($param) } elseif (strpos($param, 'counter') !== false) { return 'counter'; } - + return 'chargepoint'; // Fallback } @@ -343,10 +388,12 @@ private function isComplexParameter($param) $complexParameters = [ 'get_chargepoint_all', 'get_counter', - 'battery', 'get_battery', - 'pv', 'get_pv', + 'battery', + 'get_battery', + 'pv', + 'get_pv', 'get_chargepoint_voltages', - 'get_chargepoint_currents', + 'get_chargepoint_currents', 'get_chargepoint_powers', 'get_counter_voltages', 'get_counter_currents', @@ -355,7 +402,7 @@ private function isComplexParameter($param) 'get_battery_currents', 'get_pv_currents' ]; - + return in_array($param, $complexParameters); } @@ -366,14 +413,14 @@ private function isChargepointParameter($param) { $chargepointParameters = [ 'set_chargemode', - 'chargecurrent', + 'chargecurrent', 'minimal_pv_soc', 'minimal_permanent_current', 'max_price_eco', 'chargepoint_lock', 'bat_mode' ]; - + return in_array($param, $chargepointParameters) || strpos($param, 'chargepoint') !== false; } @@ -400,16 +447,16 @@ private function formatRawOutput($data) if (is_array($data)) { $firstKey = array_keys($data)[0]; $firstValue = $data[$firstKey]; - + if (is_array($firstValue) && count($firstValue) === 1) { return array_values($firstValue)[0]; } } - + return $data; } } // API instanziieren und Request verarbeiten $api = new SimpleAPI(); -$api->handleRequest(); \ No newline at end of file +$api->handleRequest(); diff --git a/simpleAPI/src/Authenticator.php b/simpleAPI/src/Authenticator.php index 0be8542d01..f0047d6648 100644 --- a/simpleAPI/src/Authenticator.php +++ b/simpleAPI/src/Authenticator.php @@ -56,7 +56,7 @@ private function isAuthRequired() private function checkBearerToken() { $headers = $this->getAuthorizationHeader(); - + if (!$headers) { return false; } @@ -113,7 +113,7 @@ private function checkParameterToken($params) private function validateToken($token) { $validTokens = $this->config['auth']['tokens'] ?? []; - + foreach ($validTokens as $validToken) { if (hash_equals($validToken, $token)) { return true; @@ -129,7 +129,7 @@ private function validateToken($token) private function validateCredentials($username, $password) { $users = $this->config['auth']['users'] ?? []; - + if (!isset($users[$username])) { return false; } @@ -160,10 +160,10 @@ private function getAuthorizationHeader() } elseif (function_exists('apache_request_headers')) { $requestHeaders = apache_request_headers(); $requestHeaders = array_combine( - array_map('ucwords', array_keys($requestHeaders)), + array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders) ); - + if (isset($requestHeaders['Authorization'])) { $headers = trim($requestHeaders['Authorization']); } @@ -178,7 +178,7 @@ private function getAuthorizationHeader() 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'); + $_SERVER['SERVER_PORT'] == 443 || + (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'); } -} \ No newline at end of file +} diff --git a/simpleAPI/src/MqttClient.php b/simpleAPI/src/MqttClient.php index 455a39bc55..664ec6a591 100644 --- a/simpleAPI/src/MqttClient.php +++ b/simpleAPI/src/MqttClient.php @@ -13,7 +13,7 @@ class MqttClient private $username; private $password; private $clientid; - + // Cache für bekannte IDs private static $knownIds = []; @@ -32,9 +32,9 @@ public function __construct($config) public function connect() { // Test-Verbindung mit mosquitto_sub - $cmd = $this->buildMosquittoCommand('sub', 'test/connection', '', ['-C', '1', '-W', '1']); + $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 ?? ''); } @@ -44,14 +44,14 @@ public function connect() */ public function getValue($topic) { - $cmd = $this->buildMosquittoCommand('sub', $topic, '', ['-C', '1', '-W', '1']); // Timeout auf 1 Sekunde reduziert + $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"); } - + return $value; } @@ -61,36 +61,20 @@ public function getValue($topic) public function getMultipleValues($topics) { $results = []; - - // Alle Topics in einem mosquitto_sub Aufruf mit kurzem Timeout - $topicList = implode(' -t ', array_map('escapeshellarg', $topics)); - - $cmd = sprintf( - "mosquitto_sub -h %s -p %d -t %s -C %d -W 1 -v 2>/dev/null", - escapeshellarg($this->server), - $this->port, - $topicList, - count($topics) - ); - - // 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)); - } - + + $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; } @@ -102,22 +86,22 @@ 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 = []) + private function buildMosquittoCommand($type, $topics, $message = '', $count = null, $timeout = 1, $extraArgs = []) { $binary = $type === 'sub' ? 'mosquitto_sub' : 'mosquitto_pub'; - + $cmd = sprintf( "%s -h %s -p %d", $binary, @@ -132,20 +116,38 @@ private function buildMosquittoCommand($type, $topic, $message = '', $extraArgs if (!empty($this->password)) { $cmd .= sprintf(" -P %s", escapeshellarg($this->password)); } - - // Topic hinzufügen - $cmd .= sprintf(" -t %s", escapeshellarg($topic)); - + + // 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; } @@ -156,31 +158,20 @@ public function findAvailableIds($type) { // MQTT Wildcard verwenden um alle Topics zu finden $pattern = "openWB/{$type}/+/get/imported"; - - $cmd = sprintf( - "mosquitto_sub -h %s -p %d -t %s -C 100 -W 1 -v 2>/dev/null", - escapeshellarg($this->server), - $this->port, - escapeshellarg($pattern) - ); - - if (!empty($this->username)) { - $cmd .= sprintf(" -u %s", escapeshellarg($this->username)); - } - if (!empty($this->password)) { - $cmd .= sprintf(" -P %s", escapeshellarg($this->password)); - } - + + $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("/openWB\/{$type}\/(\d+)\/get\/imported\s+(.+)/", $line, $matches)) { $id = intval($matches[1]); $value = trim($matches[2]); - + // Nur IDs mit gültigen Werten (nicht null oder leer) if ($value !== '' && $value !== 'null' && is_numeric($value)) { $ids[] = $id; @@ -188,13 +179,13 @@ public function findAvailableIds($type) } } } - + $ids = array_unique($ids); - + if (empty($ids)) { throw new \Exception("No {$type} devices found via MQTT wildcard scan"); } - + return $ids; } @@ -207,10 +198,10 @@ public function getLowestId($type) 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); @@ -218,10 +209,10 @@ public function getLowestId($type) self::$knownIds[$type] = $lowestId; return $lowestId; } - + return null; } - + /** * Verbindung schließen (dummy für Kompatibilität) */ @@ -234,4 +225,4 @@ public function __destruct() { $this->disconnect(); } -} \ No newline at end of file +} diff --git a/simpleAPI/src/MqttClientSimple.php b/simpleAPI/src/MqttClientSimple.php index 0de044cbc2..26848fbee2 100644 --- a/simpleAPI/src/MqttClientSimple.php +++ b/simpleAPI/src/MqttClientSimple.php @@ -31,7 +31,7 @@ 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 ?? ''); } @@ -44,11 +44,11 @@ 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; } @@ -59,12 +59,12 @@ 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; } @@ -74,14 +74,14 @@ public function setValue($topic, $value) 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)); @@ -89,20 +89,20 @@ private function buildMosquittoCommand($type, $topic, $message = '', $extraArgs 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; } @@ -113,12 +113,12 @@ 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; } @@ -127,11 +127,11 @@ public function findAvailableIds($type) continue; } } - + if (empty($ids)) { throw new \Exception("No {$type} devices found via MQTT"); } - + return $ids; } @@ -156,4 +156,4 @@ public function __destruct() { $this->disconnect(); } -} \ No newline at end of file +} diff --git a/simpleAPI/src/ParameterHandler.php b/simpleAPI/src/ParameterHandler.php index 3865e2b1f6..e2f2d107cf 100644 --- a/simpleAPI/src/ParameterHandler.php +++ b/simpleAPI/src/ParameterHandler.php @@ -2,6 +2,8 @@ namespace SimpleAPI; +use Exception; + /** * Handler für das Lesen und Schreiben von Parametern über MQTT */ @@ -22,47 +24,47 @@ public function readParameter($param, $id) 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 + + // Chargepoint - Einzelwerte case 'get_chargepoint_imported': return $this->getChargepointImported($id); case 'get_chargepoint_exported': @@ -83,8 +85,8 @@ public function readParameter($param, $id) return $this->getChargepointChargeState($id); case 'get_chargepoint_chargemode': return $this->getChargepointChargemode($id); - - // Counter - Einzelwerte + + // Counter - Einzelwerte case 'get_counter_voltage_p1': return $this->getCounterVoltageP1($id); case 'get_counter_voltage_p2': @@ -121,8 +123,8 @@ public function readParameter($param, $id) return $this->getCounterFaultStr($id); case 'get_counter_fault_state': return $this->getCounterFaultState($id); - - // Battery - Zusätzliche Einzelwerte + + // Battery - Zusätzliche Einzelwerte case 'get_battery': return $this->getBattery($id); case 'get_battery_power': @@ -145,8 +147,8 @@ public function readParameter($param, $id) return $this->getBatteryFaultState($id); case 'get_battery_power_limit_controllable': return $this->getBatteryPowerLimitControllable($id); - - // PV - Zusätzliche Einzelwerte + + // PV - Zusätzliche Einzelwerte case 'get_pv': return $this->getPv($id); case 'get_pv_power': @@ -165,7 +167,7 @@ public function readParameter($param, $id) return $this->getPvFaultStr($id); case 'get_pv_fault_state': return $this->getPvFaultState($id); - + default: return null; } @@ -206,12 +208,12 @@ public function writeParameter($param, $value, $chargepointId = null) 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 . 'currents', $prefix . 'powers', $prefix . 'state_str', $prefix . 'fault_str', @@ -227,9 +229,9 @@ private function getChargepointAll($id) $prefix . 'evse_current', "openWB/chargepoint/{$id}/set/charge_template" ]; - + $values = $this->mqttClient->getMultipleValues($topics); - + // Arrays parsen try { $voltages = json_decode($values[$prefix . 'voltages'] ?? '[]', true) ?: [0, 0, 0]; @@ -240,7 +242,7 @@ private function getChargepointAll($id) $currents = [0, 0, 0]; $powers = [0, 0, 0]; } - + // Chargemode aus Template extrahieren $chargemode = 'stop'; try { @@ -249,7 +251,7 @@ private function getChargepointAll($id) } catch (Exception $e) { // Fallback } - + $data = [ "chargepoint_{$id}" => [ 'power' => floatval($values[$prefix . 'power'] ?? 0), @@ -283,24 +285,24 @@ private function getChargepointAll($id) 'chargemode' => $chargemode ] ]; - + // manual_lock Status auslesen $manualLockTopic = "openWB/chargepoint/{$id}/set/manual_lock"; $manualLock = $this->mqttClient->getValue($manualLockTopic); $data["chargepoint_{$id}"]['manual_lock'] = $this->parseBooleanValue($manualLock ?? 'false'); - + return $data; } - + /** * Boolean-Wert parsen */ - private function parseBooleanValue($value) + private function parseBooleanValue($value) { if (is_bool($value)) { return $value; } - + $value = strtolower(trim($value, '"')); return in_array($value, ['true', '1', 'yes', 'on']); } @@ -313,11 +315,11 @@ 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) @@ -339,10 +341,10 @@ 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' => [ @@ -369,11 +371,11 @@ 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) @@ -395,10 +397,10 @@ 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' => [ @@ -424,7 +426,7 @@ private function getChargepointPower($id) { $topic = "openWB/chargepoint/{$id}/get/power"; $power = $this->getNumericValue($topic); - + return [ "chargepoint_{$id}" => [ 'power' => $power @@ -439,10 +441,10 @@ 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' => [ @@ -467,7 +469,7 @@ private function getChargepointPowers($id) private function getBattery($id) { $prefix = "openWB/bat/{$id}/get/"; - + // Alle benötigten Topics in einem Aufruf abfragen $topics = [ $prefix . 'power', @@ -481,16 +483,16 @@ private function getBattery($id) $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), @@ -517,7 +519,7 @@ private function getBattery($id) private function getPv($id) { $prefix = "openWB/pv/{$id}/get/"; - + // Alle benötigten Topics in einem Aufruf abfragen $topics = [ $prefix . 'power', @@ -529,16 +531,16 @@ private function getPv($id) $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), @@ -563,12 +565,12 @@ private function getPv($id) 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 . 'currents', $prefix . 'powers', $prefix . 'power_factors', $prefix . 'frequency', @@ -579,9 +581,9 @@ private function getCounter($id) $prefix . 'fault_str', $prefix . 'fault_state' ]; - + $values = $this->mqttClient->getMultipleValues($topics); - + // Arrays parsen try { $voltages = json_decode($values[$prefix . 'voltages'] ?? '[]', true) ?: [0, 0, 0]; @@ -594,7 +596,7 @@ private function getCounter($id) $powers = [0, 0, 0]; $power_factors = [0, 0, 0]; } - + return [ "counter_{$id}" => [ 'power' => floatval($values[$prefix . 'power'] ?? 0), @@ -637,49 +639,48 @@ private function setChargemode($chargepointId, $mode) // Gültige Modi mapping $validModes = [ 'instant' => 'instant_charging', - 'pv' => 'pv_charging', + 'pv' => 'pv_charging', 'eco' => 'eco_charging', 'stop' => 'stop', 'target' => '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 set topic']; } - + $template = json_decode($templateJson, true); if (!$template) { return ['success' => false, 'message' => 'Invalid charge template format']; } - + // 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()]; } @@ -813,22 +814,21 @@ 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/pv_charging/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()]; } } -} \ No newline at end of file +}