Update Websocket und weiteres.
This commit is contained in:
+19
-4
@@ -7,18 +7,24 @@
|
||||
- Vorbereitung der CSMS-Transportrolle innerhalb von IP-Symcon.
|
||||
- Routing von eingehenden OCPP-Frames nach `ChargePointId`, `EVSEId` und `ConnectorId`.
|
||||
- Entgegennahme von ausgehenden Frames aus `Ladestation_OCPP`.
|
||||
- Pufferung ausgehender Frames je `ChargePointId` fuer den WebHook/WebSocket-Spike.
|
||||
- Dokumentation des WebHook/WebSocket-Spikes.
|
||||
- Pufferung ausgehender Frames je `ChargePointId`.
|
||||
- Dokumentation des Transport-Spikes und Vorbereitung eines echten WebSocket-Adapters.
|
||||
|
||||
## Status
|
||||
|
||||
Dieses Modul ist bewusst ein Scaffold. Symcon bietet einen WebSocket Client sowie WebHook Control mit WebSocket-Support und `ProcessHookData()` fuer PHP-Module. Ob diese Mechanik fuer dauerhafte OCPP-CSMS-Verbindungen mit realen Ladestationen robust genug ist, muss mit einer OCPP-Referenzstation, einem Simulator oder dem Fronius Wattpilot getestet werden.
|
||||
Dieses Modul ist bewusst ein Scaffold. Der WebHook-Pfad ist nur fuer Diagnose und Spike geeignet. Er ist kein vollwertiger OCPP-CSMS-WebSocket-Server, weil ein Charge Point fuer OCPP eine dauerhafte bidirektionale WebSocket-Session und asynchronen CSMS-Push erwartet.
|
||||
|
||||
Eingehende Calls werden an `Ladestation_OCPP` geroutet. Die dort erzeugten OCPP-CallResults werden in `OCPP_Server` gepuffert und fuer denselben `ChargePointId` wieder ausgegeben, sofern der Symcon-Hook diesen Rueckkanal bereitstellt.
|
||||
Produktiv muss `TransportMode` auf einen echten WebSocket-Transport zeigen:
|
||||
|
||||
- `external_websocket_adapter`: lokaler Adapter/Service uebernimmt WebSocket-Handshake, Sessions, Ping/Pong und Push.
|
||||
- `symcon_websocket_parent`: spaeterer Symcon-Parent/Splitter mit echter WebSocket-Server-Funktion.
|
||||
|
||||
`webhook_spike` bleibt nur fuer Tests, bei denen eingehende Frames synchron verarbeitet und direkte CallResults zurueckgegeben werden. RemoteStart, SetChargingProfile und ChangeAvailability koennen damit nicht zuverlaessig aktiv gepusht werden.
|
||||
|
||||
## Konfiguration
|
||||
|
||||
- `HookPath`: Standard `/hook/ocpp`
|
||||
- `TransportMode`: `webhook_spike`, `external_websocket_adapter` oder `symcon_websocket_parent`
|
||||
- `DefaultTargetInstance`: Zielinstanz, wenn kein spezifisches Routing gefunden wird
|
||||
- `Ladepunkte`: optionale Routingliste je ChargePoint/EVSE/Connector
|
||||
- `HeartbeatSeconds`: Watchdog-Basis
|
||||
@@ -26,3 +32,12 @@ Eingehende Calls werden an `Ladestation_OCPP` geroutet. Die dort erzeugten OCPP-
|
||||
## Zusammenspiel
|
||||
|
||||
`Ladestation_OCPP` bleibt das fachliche Ladepunktmodul und setzt den EMS-Vertrag um. `OCPP_Server` bleibt Transport/Routing. Mehrere Ladepunkte koennen spaeter ueber denselben Transport angebunden werden.
|
||||
|
||||
## Adapter-Vertrag
|
||||
|
||||
Ein echter WebSocket-Transport muss die dauerhafte Session halten und nur Frames an `OCPP_Server` koppeln:
|
||||
|
||||
- Inbound vom Charge Point: `RouteInboundFrame` oder `ReceiveExternalFrame` mit JSON `{"ChargePointId":"...","Frame":"[...]","Remote":"ip:port"}` aufrufen.
|
||||
- Rueckgabe von `RouteInboundFrame` sofort als OCPP-CallResult/CallError ueber dieselbe WebSocket-Session senden, falls nicht leer.
|
||||
- Outbound vom CSMS: `DequeueOutboundFrame(<ChargePointId>)` abrufen und ueber die bestehende WebSocket-Session aktiv an den Charge Point senden.
|
||||
- Der Adapter muss WebSocket-Handshake, Ping/Pong, Close, Reconnect, Fragmentierung und Session-Zuordnung selbst robust behandeln.
|
||||
|
||||
+11
-1
@@ -2,7 +2,17 @@
|
||||
"elements": [
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "Transport-Scaffold fuer OCPP. WebHook/WebSocket wird als technischer Spike dokumentiert."
|
||||
"caption": "Transport-Scaffold fuer OCPP. WebHook ist nur ein Spike; produktiv ist ein echter WebSocket-Transport erforderlich."
|
||||
},
|
||||
{
|
||||
"type": "Select",
|
||||
"name": "TransportMode",
|
||||
"caption": "Transportmodus",
|
||||
"options": [
|
||||
{ "caption": "WebHook Spike / Diagnose", "value": "webhook_spike" },
|
||||
{ "caption": "Externer WebSocket Adapter", "value": "external_websocket_adapter" },
|
||||
{ "caption": "Symcon WebSocket Parent/Splitter", "value": "symcon_websocket_parent" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
|
||||
@@ -8,13 +8,14 @@ class OCPPFrameRouter
|
||||
if (!is_array($route)) {
|
||||
continue;
|
||||
}
|
||||
if ((string)($route['ChargePointId'] ?? '') !== $chargePointId) {
|
||||
$routeChargePointId = (string)($route['ChargePointId'] ?? ($route['chargePointId'] ?? ''));
|
||||
if ($routeChargePointId !== $chargePointId) {
|
||||
continue;
|
||||
}
|
||||
$routeEvse = (int)($route['EVSEId'] ?? 1);
|
||||
$routeConnector = (int)($route['ConnectorId'] ?? 1);
|
||||
$routeEvse = (int)($route['EVSEId'] ?? ($route['evseId'] ?? 1));
|
||||
$routeConnector = (int)($route['ConnectorId'] ?? ($route['connectorId'] ?? 1));
|
||||
if ($routeEvse === $evseId && $routeConnector === $connectorId) {
|
||||
return (int)($route['TargetInstance'] ?? 0);
|
||||
return (int)($route['TargetInstance'] ?? ($route['instanceId'] ?? 0));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,22 @@
|
||||
|
||||
class WebSocketEndpoint
|
||||
{
|
||||
public static function supportSummary(bool $registerHookAvailable, string $hookPath): array
|
||||
public static function supportSummary(bool $registerHookAvailable, string $hookPath, string $transportMode = 'webhook_spike'): array
|
||||
{
|
||||
if ($transportMode !== 'webhook_spike') {
|
||||
return [
|
||||
'status' => 'Externer WebSocket-Transport',
|
||||
'detail' => 'OCPP_Server erwartet einen echten WebSocket-Server/Parent oder lokalen Adapter und uebernimmt nur Routing und Protokollzustand.',
|
||||
'warning' => 'WebHook Control wird in diesem Modus nicht als OCPP-CSMS-Endpunkt verwendet.',
|
||||
'hookPath' => $hookPath
|
||||
];
|
||||
}
|
||||
|
||||
if ($registerHookAvailable) {
|
||||
return [
|
||||
'status' => 'Hook vorbereitet',
|
||||
'detail' => 'Symcon RegisterHook ist verfuegbar. WebSocket-Dauerbetrieb muss mit echter OCPP-Station verifiziert werden.',
|
||||
'status' => 'WebHook-Spike',
|
||||
'detail' => 'RegisterHook ist verfuegbar, aber der WebHook-Pfad ist kein vollwertiger OCPP-CSMS-WebSocket-Server mit garantiertem Async-Push.',
|
||||
'warning' => 'Nur fuer Spike/Diagnose verwenden. Produktiv ist ein echter WebSocket-Server/Parent erforderlich.',
|
||||
'hookPath' => $hookPath
|
||||
];
|
||||
}
|
||||
@@ -15,6 +25,7 @@ class WebSocketEndpoint
|
||||
return [
|
||||
'status' => 'Hook manuell/spaeter',
|
||||
'detail' => 'RegisterHook ist in dieser Symcon-Umgebung nicht als Modul-Methode verfuegbar. WebHook Control muss manuell oder nach Upgrade verbunden werden.',
|
||||
'warning' => 'Kein produktiver OCPP-WebSocket-Transport aktiv.',
|
||||
'hookPath' => $hookPath
|
||||
];
|
||||
}
|
||||
|
||||
+33
-10
@@ -13,6 +13,7 @@ class OCPP_Server extends IPSModule
|
||||
{
|
||||
parent::Create();
|
||||
|
||||
$this->RegisterPropertyString('TransportMode', 'webhook_spike');
|
||||
$this->RegisterPropertyBoolean('EnableWebhook', true);
|
||||
$this->RegisterPropertyString('HookPath', '/hook/ocpp');
|
||||
$this->RegisterPropertyInteger('DefaultTargetInstance', 0);
|
||||
@@ -30,6 +31,9 @@ class OCPP_Server extends IPSModule
|
||||
$this->RegisterVariableInteger('ConnectionCount', 'ConnectionCount', '', 30);
|
||||
$this->RegisterVariableInteger('LastMessageTime', 'LastMessageTime', '', 31);
|
||||
$this->RegisterVariableInteger('OutboundQueueCount', 'OutboundQueueCount', '', 32);
|
||||
$this->RegisterVariableString('TransportModeStatus', 'TransportModeStatus', '', 33);
|
||||
$this->RegisterVariableBoolean('OutboundPushSupported', 'OutboundPushSupported', '~Switch', 34);
|
||||
$this->RegisterVariableString('TransportWarning', 'TransportWarning', '', 35);
|
||||
$this->RegisterVariableString('WebSocketSupportStatus', 'WebSocketSupportStatus', '', 40);
|
||||
$this->RegisterVariableString('LetzteMeldung', 'LetzteMeldung', '', 41);
|
||||
$this->RegisterVariableInteger('LetzteMeldungZeit', 'LetzteMeldungZeit', '', 42);
|
||||
@@ -46,11 +50,15 @@ class OCPP_Server extends IPSModule
|
||||
parent::ApplyChanges();
|
||||
|
||||
$this->SetTimerInterval('Timer_TransportWatchdog', max(5, $this->ReadPropertyInteger('HeartbeatSeconds')) * 1000);
|
||||
$summary = WebSocketEndpoint::supportSummary(method_exists($this, 'RegisterHook'), $this->ReadPropertyString('HookPath'));
|
||||
$transportMode = $this->ReadPropertyString('TransportMode');
|
||||
$summary = WebSocketEndpoint::supportSummary(method_exists($this, 'RegisterHook'), $this->ReadPropertyString('HookPath'), $transportMode);
|
||||
$this->SetValue('WebSocketSupportStatus', $summary['status'] . ': ' . $summary['detail']);
|
||||
$this->SetValue('TransportModeStatus', $transportMode);
|
||||
$this->SetValue('OutboundPushSupported', $transportMode !== 'webhook_spike');
|
||||
$this->SetValue('TransportWarning', $summary['warning']);
|
||||
$this->SetSummary($summary['status']);
|
||||
|
||||
if ($this->ReadPropertyBoolean('EnableWebhook')) {
|
||||
if ($transportMode === 'webhook_spike' && $this->ReadPropertyBoolean('EnableWebhook')) {
|
||||
$this->tryRegisterHook();
|
||||
}
|
||||
|
||||
@@ -74,12 +82,13 @@ class OCPP_Server extends IPSModule
|
||||
break;
|
||||
|
||||
case 'RouteInboundFrame':
|
||||
$this->RouteInboundFrame((string)$Value);
|
||||
break;
|
||||
return $this->RouteInboundFrame((string)$Value);
|
||||
|
||||
case 'ReceiveExternalFrame':
|
||||
return $this->RouteInboundFrame((string)$Value);
|
||||
|
||||
case 'DequeueOutboundFrame':
|
||||
$this->DequeueOutboundFrame((string)$Value);
|
||||
break;
|
||||
return $this->DequeueOutboundFrame((string)$Value);
|
||||
|
||||
case 'TransportWatchdog':
|
||||
$this->TransportWatchdog();
|
||||
@@ -101,6 +110,16 @@ class OCPP_Server extends IPSModule
|
||||
|
||||
protected function ProcessHookData($JSONString = '')
|
||||
{
|
||||
if ($this->ReadPropertyString('TransportMode') !== 'webhook_spike') {
|
||||
header('HTTP/1.1 501 Not Implemented');
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'status' => 'rejected',
|
||||
'reason' => 'ProcessHookData ist nur fuer den WebHook-Spike aktiv. Produktiver OCPP-Betrieb braucht einen echten WebSocket-Transport.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$raw = WebSocketEndpoint::readRawBody();
|
||||
if ($raw === '' && is_string($JSONString)) {
|
||||
$raw = $JSONString;
|
||||
@@ -141,7 +160,11 @@ class OCPP_Server extends IPSModule
|
||||
$this->SetValue('LastOutboundFrame', $frame);
|
||||
$this->SetValue('LastMessageTime', time());
|
||||
$this->enqueueOutboundFrame($chargePointId, $frame);
|
||||
$this->setMessage('Outbound Frame fuer ' . ($chargePointId === '' ? 'unbekannt' : $chargePointId) . ' gepuffert.');
|
||||
if ($this->ReadPropertyString('TransportMode') === 'webhook_spike') {
|
||||
$this->setMessage('Outbound Frame gepuffert, aber WebHook-Spike kann keinen echten Async-Push garantieren.');
|
||||
return;
|
||||
}
|
||||
$this->setMessage('Outbound Frame fuer echten WebSocket-Transport gepuffert: ' . ($chargePointId === '' ? 'unbekannt' : $chargePointId));
|
||||
}
|
||||
|
||||
public function RouteInboundFrame(string $json): string
|
||||
@@ -250,7 +273,7 @@ class OCPP_Server extends IPSModule
|
||||
{
|
||||
$last = (int)$this->GetValue('LastMessageTime');
|
||||
if ($last === 0) {
|
||||
$this->SetValue('TransportStatus', 'Wartet auf OCPP Verbindung');
|
||||
$this->SetValue('TransportStatus', $this->ReadPropertyString('TransportMode') === 'webhook_spike' ? 'WebHook-Spike wartet' : 'Wartet auf WebSocket Verbindung');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -261,7 +284,7 @@ class OCPP_Server extends IPSModule
|
||||
return;
|
||||
}
|
||||
|
||||
$this->SetValue('TransportStatus', 'Aktiv/Scaffold');
|
||||
$this->SetValue('TransportStatus', $this->ReadPropertyString('TransportMode') === 'webhook_spike' ? 'Aktiv/WebHook-Spike' : 'Aktiv/WebSocket-Transport');
|
||||
}
|
||||
|
||||
private function tryRegisterHook(): void
|
||||
@@ -270,7 +293,7 @@ class OCPP_Server extends IPSModule
|
||||
if (method_exists($this, 'RegisterHook')) {
|
||||
try {
|
||||
$this->RegisterHook($hook);
|
||||
$this->SetValue('WebSocketSupportStatus', 'RegisterHook aufgerufen fuer ' . $hook . '. WebSocket-Dauerbetrieb noch testen.');
|
||||
$this->SetValue('WebSocketSupportStatus', 'RegisterHook aufgerufen fuer ' . $hook . '. Nur WebHook-Spike, kein produktiver OCPP-WebSocket-Dauerbetrieb.');
|
||||
$this->setMessage('Webhook registriert: ' . $hook);
|
||||
return;
|
||||
} catch (Throwable $e) {
|
||||
|
||||
Reference in New Issue
Block a user