diff --git a/simpleAPI/simpleAPI_mqtt.py b/simpleAPI/simpleAPI_mqtt.py index 0609b9e18a..821b3a3b60 100755 --- a/simpleAPI/simpleAPI_mqtt.py +++ b/simpleAPI/simpleAPI_mqtt.py @@ -116,12 +116,14 @@ def _on_connect(self, client, userdata, flags, rc): client.subscribe("openWB/pv/#", qos=0) client.subscribe("openWB/chargepoint/#", qos=0) client.subscribe("openWB/counter/#", qos=0) + client.subscribe("openWB/consumer/#", 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) and simpleAPI set topics") + "Subscribed to openWB component topics (bat, pv, chargepoint, counter, consumer) " + "and simpleAPI set topics") else: log.error(f"Failed to connect to MQTT broker. Return code: {rc}") @@ -261,8 +263,11 @@ def _parse_payload(self, payload: str) -> Any: """Parse payload as JSON, tuple, or raw value.""" payload = payload.strip() - # Try to parse as JSON - if payload.startswith('{') or payload.startswith('['): + # Try to parse as JSON. openWB JSON-encodes every payload it publishes, including plain + # strings (e.g. fault_str arrives on the wire as '"Kein Fehler."', not 'Kein Fehler.') - + # without also matching a leading '"' here, those quote characters would be republished + # literally as part of the simpleAPI value. + if payload.startswith('{') or payload.startswith('[') or payload.startswith('"'): try: return json.loads(payload) except json.JSONDecodeError as e: @@ -585,6 +590,38 @@ def _publish_charge_template_read_topics(self, chargepoint_id: str, charge_templ 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']) + except Exception as e: log.error(f"Error publishing charge_template read topics: {e}") @@ -618,6 +655,23 @@ def _handle_write_operation(self, topic: str, payload: str): 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 + topic_parts = topic_remainder.split('/') if len(topic_parts) < 1: @@ -954,6 +1008,105 @@ def _handle_instant_charging_limit_amount_operation(self, payload: str) -> bool: ) 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 + + # 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 + + 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.selected value + charge_template['chargemode']['pv_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) + log.info(f"Set pv_charging_limit to {payload} for chargepoint {chargepoint_id}") + return True + + def _handle_pv_charging_limit_soc_operation(self, payload: str) -> bool: + """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 + + 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: + """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 + def _publish_json(self, topic: str, data: Dict[str, Any]): """Publish JSON data to a topic.""" try: diff --git a/simpleAPI/simpleapi.php b/simpleAPI/simpleapi.php index 4f21b453f9..5c97ffa66b 100644 --- a/simpleAPI/simpleapi.php +++ b/simpleAPI/simpleapi.php @@ -90,6 +90,12 @@ public function handleRequest() } } + // 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)) { @@ -186,6 +192,9 @@ private function getWriteParameters($params) 'instant_charging_limit', 'instant_charging_amount', 'instant_charging_soc', + 'pv_charging_limit', + 'pv_charging_amount', + 'pv_charging_soc', 'vehicle', 'manual_soc', 'set_io_output' @@ -302,6 +311,24 @@ private function getReadParameters($params) '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', @@ -458,6 +485,8 @@ private function getTypeFromParameter($param) return 'pv'; } elseif (strpos($param, 'counter') !== false) { return 'counter'; + } elseif (strpos($param, 'consumer') !== false) { + return 'consumer'; } return 'chargepoint'; // Fallback @@ -485,7 +514,13 @@ private function isComplexParameter($param) 'get_counter_power_factors', 'get_battery_currents', 'get_pv_currents', - 'get_io_output_all' + 'get_io_output_all', + 'get_consumer_all', + 'get_consumer', + 'consumer', + 'get_consumer_currents', + 'get_consumer_voltages', + 'get_consumer_powers' ]; return in_array($param, $complexParameters); @@ -508,6 +543,9 @@ private function isChargepointParameter($param) 'instant_charging_limit', 'instant_charging_amount', 'instant_charging_soc', + 'pv_charging_limit', + 'pv_charging_amount', + 'pv_charging_soc', 'vehicle', 'manual_soc' ]; @@ -539,6 +577,12 @@ private function getSuccessMessage($param, $value, $chargepointId) 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': diff --git a/simpleAPI/src/MqttClient.php b/simpleAPI/src/MqttClient.php index 03fea79480..d715f195b7 100644 --- a/simpleAPI/src/MqttClient.php +++ b/simpleAPI/src/MqttClient.php @@ -230,20 +230,44 @@ private function buildMosquittoCommandWithCredentials($type, $topics, $message = } /** - * Alle verfügbaren IDs für einen bestimmten Typ finden (MQTT Wildcard Scan) + * 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. */ - public function findAvailableIds($type) + private function getScanConfig() { - // MQTT Wildcard verwenden um alle Topics zu finden - $scanConfig = [ + 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; + } - $config = $scanConfig[$type] ?? null; + /** + * 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}"); } @@ -264,21 +288,36 @@ public function findAvailableIds($type) $id = intval($matches[1]); $value = trim($matches[2]); - // Nur IDs mit gültigen Werten (bei IO auch JSON erlaubt) - if ($value !== '' && $value !== 'null' && ($type === 'io' || is_numeric($value))) { + // 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; + } - if (empty($ids)) { - throw new \Exception("No {$type} devices found via MQTT wildcard scan"); + /** + * 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 $ids; + return $result; } /** diff --git a/simpleAPI/src/ParameterHandler.php b/simpleAPI/src/ParameterHandler.php index 7f4ebc813b..8e886b8167 100644 --- a/simpleAPI/src/ParameterHandler.php +++ b/simpleAPI/src/ParameterHandler.php @@ -206,6 +206,39 @@ public function readParameter($param, $id, $options = []) 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': @@ -248,6 +281,12 @@ public function writeParameter($param, $value, $targetId = null, $options = []) 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': @@ -325,8 +364,13 @@ private function getChargepointAll($id) $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'; @@ -340,7 +384,12 @@ private function getChargepointAll($id) $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; @@ -403,8 +452,8 @@ private function getChargepointAll($id) floatval($powers[1] ?? 0), floatval($powers[2] ?? 0) ], - 'state_str' => $values[$prefix . 'state_str'] ?? 'Unbekannt', - 'fault_str' => $values[$prefix . 'fault_str'] ?? 'Kein Fehler', + '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), @@ -423,7 +472,7 @@ private function getChargepointAll($id) floatval($powerFactors[1] ?? 0), floatval($powerFactors[2] ?? 0) ], - 'rfid' => $values[$prefix . 'rfid'] ?? null, + 'rfid' => $this->decodeStringValue($values[$prefix . 'rfid'] ?? null, null), 'rfid_timestamp' => $values[$prefix . 'rfid_timestamp'] ?? null, 'config_name' => $configName, 'connected_vehicle_name' => $connectedVehicleName, @@ -434,6 +483,9 @@ private function getChargepointAll($id) '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), @@ -443,7 +495,11 @@ private function getChargepointAll($id) // manual_lock Status auslesen $manualLockTopic = "openWB/chargepoint/{$id}/set/manual_lock"; - $manualLock = $this->mqttClient->getValue($manualLockTopic); + try { + $manualLock = $this->mqttClient->getValue($manualLockTopic); + } catch (Exception $e) { + $manualLock = 'false'; + } $data["chargepoint_{$id}"]['manual_lock'] = $this->parseBooleanValue($manualLock ?? 'false'); return $data; @@ -462,6 +518,21 @@ private function parseBooleanValue($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 */ @@ -665,7 +736,7 @@ private function getBattery($id) 'exported' => floatval($values[$prefix . 'exported'] ?? 0), 'daily_imported' => floatval($values[$prefix . 'daily_imported'] ?? 0), 'daily_exported' => floatval($values[$prefix . 'daily_exported'] ?? 0), - 'fault_str' => $values[$prefix . 'fault_str'] ?? 'Kein Fehler', + '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') ] @@ -712,7 +783,83 @@ private function getPv($id) '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' => $values[$prefix . 'fault_str'] ?? 'Kein Fehler', + '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) ] ]; @@ -784,7 +931,7 @@ private function getCounter($id) 'daily_exported' => floatval($values[$prefix . 'daily_exported'] ?? 0), 'imported' => floatval($values[$prefix . 'imported'] ?? 0), 'daily_imported' => floatval($values[$prefix . 'daily_imported'] ?? 0), - 'fault_str' => $values[$prefix . 'fault_str'] ?? 'Kein Fehler', + 'fault_str' => $this->decodeStringValue($values[$prefix . 'fault_str'] ?? null, 'Kein Fehler'), 'fault_state' => intval($values[$prefix . 'fault_state'] ?? 0) ] ]; @@ -1026,7 +1173,7 @@ private function getChargepointRfid($id) try { $topic = "openWB/chargepoint/{$id}/get/rfid"; $value = $this->mqttClient->getValue($topic); - return ["chargepoint_{$id}" => ['rfid' => strval($value ?? '')]]; + return ["chargepoint_{$id}" => ['rfid' => $this->decodeStringValue($value)]]; } catch (Exception $e) { return ["chargepoint_{$id}" => ['rfid' => '']]; } @@ -1321,7 +1468,7 @@ private function getChargepointStateStr($id) try { $topic = "openWB/chargepoint/{$id}/get/state_str"; $value = $this->mqttClient->getValue($topic); - return ["chargepoint_{$id}" => ['state_str' => strval($value ?? '')]]; + return ["chargepoint_{$id}" => ['state_str' => $this->decodeStringValue($value)]]; } catch (Exception $e) { return ["chargepoint_{$id}" => ['state_str' => '']]; } @@ -1335,7 +1482,7 @@ private function getChargepointFaultStr($id) try { $topic = "openWB/chargepoint/{$id}/get/fault_str"; $value = $this->mqttClient->getValue($topic); - return ["chargepoint_{$id}" => ['fault_str' => strval($value ?? '')]]; + return ["chargepoint_{$id}" => ['fault_str' => $this->decodeStringValue($value)]]; } catch (Exception $e) { return ["chargepoint_{$id}" => ['fault_str' => '']]; } @@ -1405,7 +1552,7 @@ private function getChargepointChargemode($id) try { $topic = "openWB/chargepoint/{$id}/get/chargemode"; $value = $this->mqttClient->getValue($topic); - return ["chargepoint_{$id}" => ['chargemode' => strval($value ?? '')]]; + return ["chargepoint_{$id}" => ['chargemode' => $this->decodeStringValue($value)]]; } catch (Exception $e) { return ["chargepoint_{$id}" => ['chargemode' => '']]; } @@ -1520,6 +1667,115 @@ private function setInstantChargingSoc($chargepointId, $value) } } + /** + * 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 */ @@ -1946,7 +2202,7 @@ private function getCounterFaultStr($id) try { $topic = "openWB/counter/{$id}/get/fault_str"; $value = $this->mqttClient->getValue($topic); - return ["counter_{$id}" => ['fault_str' => strval($value ?? 'Kein Fehler')]]; + return ["counter_{$id}" => ['fault_str' => $this->decodeStringValue($value, 'Kein Fehler')]]; } catch (Exception $e) { return ["counter_{$id}" => ['fault_str' => 'Kein Fehler']]; } @@ -2086,7 +2342,7 @@ private function getBatteryFaultStr($id) try { $topic = "openWB/bat/{$id}/get/fault_str"; $value = $this->mqttClient->getValue($topic); - return ["battery_{$id}" => ['fault_str' => strval($value ?? 'Kein Fehler')]]; + return ["battery_{$id}" => ['fault_str' => $this->decodeStringValue($value, 'Kein Fehler')]]; } catch (Exception $e) { return ["battery_{$id}" => ['fault_str' => 'Kein Fehler']]; } @@ -2265,7 +2521,7 @@ private function getPvFaultStr($id) try { $topic = "openWB/pv/{$id}/get/fault_str"; $value = $this->mqttClient->getValue($topic); - return ["pv_{$id}" => ['fault_str' => strval($value ?? 'Kein Fehler')]]; + return ["pv_{$id}" => ['fault_str' => $this->decodeStringValue($value, 'Kein Fehler')]]; } catch (Exception $e) { return ["pv_{$id}" => ['fault_str' => 'Kein Fehler']]; } @@ -2285,6 +2541,219 @@ private function getPvFaultState($id) } } + /** + * 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 */