Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 70 additions & 3 deletions simpleAPI/simpleAPI_mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import time
import sys
import ssl
import math
from typing import Dict, Any, Optional
from pathlib import Path
import paho.mqtt.client as mqtt
Expand Down Expand Up @@ -46,6 +47,20 @@ def __init__(self, config_file: str):
# Cache for storing current charge_template configurations
self.charge_template_cache: Dict[str, Dict[str, Any]] = {}

# Track per-ID component values for efficient total topic aggregation
self.total_metrics = {
'pv': {'exported', 'monthly_exported', 'yearly_exported', 'imported', 'power'},
'chargepoint': {'exported', 'monthly_exported', 'yearly_exported', 'imported', 'power'}
}
self.total_value_cache: Dict[str, Dict[str, Dict[int, float]]] = {
component: {metric: {} for metric in metrics}
for component, metrics in self.total_metrics.items()
}
self.total_sums: Dict[str, Dict[str, float]] = {
component: {metric: 0.0 for metric in metrics}
for component, metrics in self.total_metrics.items()
}

# MQTT client setup
self.client = mqtt.Client()
self.client.on_connect = self._on_connect
Expand Down Expand Up @@ -161,19 +176,26 @@ def _on_message(self, client, userdata, msg):
if '/set/charge_template' in topic:
self._cache_charge_template(topic, payload)

# Parse payload once so we can reuse it for both total aggregation and normal transformation
parsed_payload = self._parse_payload(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)
self._transform_and_publish(topic, payload, 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):
def _transform_and_publish(self, original_topic: str, payload: str, parsed_payload: Any = None):
"""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)
if parsed_payload is None:
parsed_payload = self._parse_payload(payload)

# Generate transformed topics
transformed_topics = self._generate_simple_topics(original_topic, parsed_payload)
Expand All @@ -190,6 +212,51 @@ def _transform_and_publish(self, original_topic: str, payload: str):
except Exception as e:
log.error(f"Error transforming topic {original_topic}: {e}")

def _update_total_topics(self, original_topic: str, parsed_value: Any):
"""Maintain and publish aggregated total topics from all numeric component IDs."""
# Match only per-ID get topics needed for totals
# Examples:
# openWB/pv/7/get/exported
# openWB/chargepoint/13/get/power
pattern = r'^openWB/(pv|chargepoint)/(\d+)/get/(exported|monthly_exported|yearly_exported|imported|power)$'
match = re.match(pattern, original_topic)
if not match:
return

component_type = match.group(1)
component_id = int(match.group(2))
metric = match.group(3)

# Only numeric, finite values are aggregated
if isinstance(parsed_value, bool) or not isinstance(parsed_value, (int, float)):
return

numeric_value = float(parsed_value)
if not math.isfinite(numeric_value):
return

metric_cache = self.total_value_cache[component_type][metric]
previous_value = metric_cache.get(component_id, 0.0)
metric_cache[component_id] = numeric_value

# O(1) sum update for performance with many IDs and frequent updates
self.total_sums[component_type][metric] += numeric_value - previous_value
total_value = self.total_sums[component_type][metric]

total_topic = self._build_total_topic(component_type, metric)
if total_topic is not None:
self._publish_if_changed(total_topic, total_value)

def _build_total_topic(self, component_type: str, metric: str) -> Optional[str]:
"""Build destination topic for aggregated totals with component-consistent path style."""
if component_type == 'pv':
# PV topics keep /get/ in simpleAPI
return f"openWB/simpleAPI/pv/total/get/{metric}"
if component_type == 'chargepoint':
# chargepoint topics are published without /get/ in simpleAPI
return f"openWB/simpleAPI/chargepoint/total/{metric}"
return None

def _parse_payload(self, payload: str) -> Any:
"""Parse payload as JSON, tuple, or raw value."""
payload = payload.strip()
Expand Down
71 changes: 58 additions & 13 deletions simpleAPI/simpleapi.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ private function getWriteParameters($params)
'instant_charging_amount',
'instant_charging_soc',
'vehicle',
'manual_soc'
'manual_soc',
'set_io_output'
];

foreach ($writeableKeys as $key) {
Expand Down Expand Up @@ -225,6 +226,8 @@ private function getReadParameters($params)
'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',
Expand Down Expand Up @@ -291,12 +294,16 @@ private function getReadParameters($params)
// 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',
// IO - Ausgaenge
'get_io_output_all',
'get_io_output',
// System - Werte
'get_lastlivevaluesjson'
];
Expand All @@ -315,28 +322,47 @@ private function getReadParameters($params)
*/
private function handleWriteRequest($writeParams, $allParams)
{
$firstTargetId = null;

foreach ($writeParams as $param => $value) {
$chargepointId = $allParams['chargepoint_nr'] ?? null;
$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';
}

// Auto-ID Feature: Niedrigste ID finden wenn keine angegeben
if ($chargepointId === null && $this->isChargepointParameter($param)) {
if ($idType !== null && ($targetId === null || $targetId === '' || $targetId === 'auto')) {
try {
$chargepointId = $this->mqttClient->getLowestId('chargepoint');
if ($chargepointId === null) {
$targetId = $this->mqttClient->getLowestId($idType);
if ($targetId === null) {
return [
'success' => false,
'message' => 'No chargepoints available for auto-ID'
'message' => "No {$idType} devices available for auto-ID"
];
}
} catch (Exception $e) {
return [
'success' => false,
'message' => 'Auto-ID failed: ' . $e->getMessage()
'message' => "Auto-ID failed for {$idType}: " . $e->getMessage()
];
}
}

$result = $this->parameterHandler->writeParameter($param, $value, $chargepointId);
$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;
Expand All @@ -349,9 +375,10 @@ private function handleWriteRequest($writeParams, $allParams)

return [
'success' => true,
'message' => $this->getSuccessMessage($firstParam, $firstValue, $chargepointId ?? null),
'message' => $this->getSuccessMessage($firstParam, $firstValue, $firstTargetId ?? null),
'data' => [
'chargepoint_nr' => $chargepointId,
'chargepoint_nr' => $this->isChargepointParameter($firstParam) ? $firstTargetId : null,
'io_nr' => $this->isIoParameter($firstParam) ? $firstTargetId : null,
$firstParam => $firstValue
]
];
Expand Down Expand Up @@ -381,7 +408,12 @@ private function handleReadRequest($readParams, $allParams, $debugInfo = [])
}
}

$data = $this->parameterHandler->readParameter($param, $id);
$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);
}
Expand Down Expand Up @@ -417,6 +449,8 @@ 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) {
Expand Down Expand Up @@ -449,7 +483,8 @@ private function isComplexParameter($param)
'get_counter_powers',
'get_counter_power_factors',
'get_battery_currents',
'get_pv_currents'
'get_pv_currents',
'get_io_output_all'
];

return in_array($param, $complexParameters);
Expand Down Expand Up @@ -478,6 +513,14 @@ private function isChargepointParameter($param)
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
*/
Expand All @@ -498,6 +541,8 @@ private function getSuccessMessage($param, $value, $chargepointId)
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}.";
}
Expand Down
45 changes: 41 additions & 4 deletions simpleAPI/src/MqttClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,29 @@ public function getMultipleValues($topics)
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
*/
Expand Down Expand Up @@ -212,7 +235,21 @@ private function buildMosquittoCommandWithCredentials($type, $topics, $message =
public function findAvailableIds($type)
{
// MQTT Wildcard verwenden um alle Topics zu finden
$pattern = "openWB/{$type}/+/get/imported";
$scanConfig = [
'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+(.+)/'],
'io' => ['pattern' => 'openWB/io/states/+/get/digital_output', 'regex' => '/openWB\/io\/states\/(\d+)\/get\/digital_output\s+(.+)/']
];

$config = $scanConfig[$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';
Expand All @@ -223,12 +260,12 @@ public function findAvailableIds($type)
if ($output) {
$lines = explode("\n", trim($output));
foreach ($lines as $line) {
if (preg_match("/openWB\/{$type}\/(\d+)\/get\/imported\s+(.+)/", $line, $matches)) {
if (preg_match($matchRegex, $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)) {
// Nur IDs mit gültigen Werten (bei IO auch JSON erlaubt)
if ($value !== '' && $value !== 'null' && ($type === 'io' || is_numeric($value))) {
$ids[] = $id;
}
}
Expand Down
Loading
Loading