REST API (v1)
Handbuch zur v1 REST API von PlantPulse Edge. Standardschnittstelle, über die Oberflächen und Fremdsysteme OPC, Tags, Monitoring und Healthchecks der externen Übertragung ansprechen.
| Punkt | Wert |
|---|---|
| Base URL | http(s)://<edge-host>/api/v1 |
| Content-Type | application/json; charset=utf-8 |
| Authentifizierung | API Key erzwungen (edge.rest.api.auth=true) — X-API-Key / Authorization: Bearer, oder Durchlass per Login-Session. ?api_key= wird mit 401 abgewiesen |
| Audit-Log | ApiAccessLogFilter — protokolliert jeden /api/*-Aufruf in einer Zeile (Methode/Pfad/Status/Dauer/IP/Benutzer/auth-Modus) |
| Haupt-Controller | plantpulse.app.edge.api.v1.* (OpcAPI, TagAPI, AppAPI, SystemAPI, MonitoringAPI) |
| Fehler-Envelope | {data, meta, errors} — auch auf Filter-/Advice-Pfaden identisch |
| HTTPS | edge.rest.api.https_only=true standardmäßig. Aufrufe von Clients über reines HTTP werden abgewiesen oder folgen der HTTPS-Redirect-Policy von Tomcat |
Authentifizierung (ApiAuthFilter)
Aktivierung in app.properties über edge.rest.api.auth=true. Der Schlüssel ist edge.rest.api.key und wird im Betrieb über EDGE_REST_API_KEY_FILE oder EDGE_REST_API_KEY injiziert.
Der Zugriff wird gewährt, wenn mindestens eine der folgenden 3 Bedingungen erfüllt ist:
A. Durchlass per Login-Session (Browser)
Benutzer, die bereits über /login/form angemeldet sind — enthält die Session das Attribut _USER_LOGIN (JSONObject), führt der ApiAuthFilter direkt chain.doFilter() aus. Damit funktionieren alle per Ajax aus der Oberfläche (JSP) aufgerufenen /api/* ohne zusätzlichen Schlüssel.
B. Header X-API-Key (empfohlen)
EDGE_REST_API_KEY="$(tr -d '\r\n' < /run/secrets/edge-rest-api-key)"
cfg="$(mktemp)"
trap 'rm -f "$cfg"' EXIT
printf 'header = "X-API-Key: %s"\n' "$EDGE_REST_API_KEY" > "$cfg"
curl --config "$cfg" https://edge.example.com/api/v1/tag/TAG_LS_XBC_0001/value
C. Header Authorization: Bearer <key>
EDGE_REST_API_KEY="$(tr -d '\r\n' < /run/secrets/edge-rest-api-key)"
cfg="$(mktemp)"
trap 'rm -f "$cfg"' EXIT
printf 'header = "Authorization: Bearer %s"\n' "$EDGE_REST_API_KEY" > "$cfg"
curl --config "$cfg" https://edge.example.com/api/v1/tag/TAG_LS_XBC_0001/value
D. Query-Parameter ?api_key=<key> — abgewiesen
Da der Schlüssel im Klartext in der URL steht und so in Access-Logs, Proxy-Caches oder im Browser-Verlauf verbleiben kann, wird er in v1 mit 401 abgewiesen. Verwenden Sie die Header-Varianten (B/C).
Fehlerantwort
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
{
"data": null,
"meta": {
"timestamp": 1778069486934,
"status": "ERROR",
"http_status": 401,
"request_id": "3b89b8f1-0fc1-4ab4-b4eb-d12dc314d500"
},
"errors": [
{
"code": "UNAUTHORIZED",
"message": "Invalid or missing X-API-Key."
}
]
}
Der Schlüsselvergleich erfolgt per constantTimeEquals() (Schutz vor Timing-Angriffen).
edge.rest.api.auth=false (Entwicklung / temporär)
Ist die Authentifizierung deaktiviert, führt der ApiAuthFilter sofort chain.doFilter() aus — externe Aufrufe sind ohne Schlüssel möglich. Im Produktivnetz stets true empfohlen.
Audit-Log (ApiAccessLogFilter)
Protokolliert jeden /api/*-Aufruf in einer Logzeile — auch fehlgeschlagene Authentifizierungen (401) werden erfasst und hinterlassen einen Audit-Trail (Reihenfolge der Filterkette: ApiAccessLogFilter → ApiAuthFilter).
Format
[API_ACCESS] method=GET path=/api/v1/tag/TAG_MS_1000/value query= status=200 duration=12ms
ip=100.127.147.98 user=admin auth=session ua=Mozilla/5.0...
| Feld | Bedeutung |
|---|---|
method | HTTP-Methode |
path | Request-URI (ohne Query) |
query | Query-String — auch der abgewiesene ?api_key=...-Teil wird mit *** maskiert |
status | HTTP-Antwortcode |
duration | Verarbeitungszeit (ms) |
ip | Standardmäßig RemoteAddr. Nur bei Reverse-Proxy-Betrieb mit edge.rest.api.trust_x_forwarded_for=true die erste IP aus X-Forwarded-For |
user | Bei Session-Benutzer user_id / bei apikey XXXX*** (len=16) (erste 4 Zeichen + Länge) / bei anonym - |
auth | session (Browser-Login) / apikey (X-API-Key/Bearer) / deprecated_query (abgewiesener ?api_key=-Versuch) / none (Schlüssel fehlt oder anonym) |
ua | User-Agent (bei mehr als 80 Zeichen mit … abgeschnitten) |
error | Bei Exception wird error=ClassName ergänzt |
Verzweigung der Log-Level
| Status | Level | Zweck |
|---|---|---|
| 5xx oder throwable | ERROR | Serverfehler — sofortiger Alarm |
| 401, 403 | WARN | Authentifizierungs-/Berechtigungsablehnung — Audit hat Vorrang |
| Sonstige (2xx, 3xx, allgemeine 4xx) | INFO | Normale Aufrufe |
Anwendung
- Wer sendet falsche Schlüssel — 401-Trail per
grep '\[API_ACCESS\].*status=401'prüfen - 5xx-Regressionen nachverfolgen —
grep '\[API_ACCESS\].*status=5'+ Felderror= - Aufrufhäufigkeit / Auswertung pro Benutzer — Gruppierung nach auth=apikey/session, Analyse pro ip
- Langsame Antworten / Slow Queries — Zeilen mit hohem
duration=extrahieren
Der Schlüssel erscheint nirgends im Klartext im Log — Query-Maskierung plus die ersten 4 Zeichen im user-Feld genügen zur Identifikation. Ein ausgewogenes Verhältnis aus ausreichender Identifizierbarkeit für die Vorfallanalyse und Schutz vor Klartextoffenlegung.
Mapping-Reihenfolge in web.xml: zuerst ApiAccessLogFilter, danach ApiAuthFilter. Da der AccessLog den Status im finally-Block protokolliert, werden auch mit 401 abgelehnte Aufrufe vollständig auditiert. Bei umgekehrter Reihenfolge fehlen 401-Aufrufe im Access-Log.
Antwort-Envelope
Antworten der v1 REST API werden in einem gemeinsamen Envelope gekapselt (RestApiSupport, ApiEnvelopeWriter).
Normale Antwort:
{
"data": { },
"meta": {
"timestamp": 1778069486934,
"status": "OK",
"request_id": "f818abd0-eb0f-4497-9b07-7ccc94b2e2d7"
}
}
Fehlerantwort:
{
"data": null,
"meta": {
"timestamp": 1778069486934,
"status": "ERROR",
"http_status": 400,
"request_id": "3b89b8f1-0fc1-4ab4-b4eb-d12dc314d500"
},
"errors": [
{
"code": "VALIDATION_FAILED",
"message": "Validation failed"
}
]
}
Der alte Envelope im RPC-Stil (result, _session_id, _user_id) wird in REST v1 nicht mehr verwendet.
1. Übersicht der Endpunkte
1.1 OPC
| Slug | Method | URL | Beschreibung |
|---|---|---|---|
flow_edge_opc_list | GET | /api/v1/opc | OPC-Liste (inkl. tag_count / connection_status / scan_status) |
flow_edge_opc_create | POST | /api/v1/opc | OPC + Tags gebündelt anlegen |
flow_edge_opc_update | PUT | /api/v1/opc/{opcId} | OPC + Tags gebündelt aktualisieren (opcId aus dem Pfad überschreibt opc_id im Body) |
flow_edge_opc_delete | DELETE | /api/v1/opc/{opcId} | OPC und alle untergeordneten Tags gemeinsam löschen |
flow_edge_opc_start | POST | /api/v1/opc/{opcId}/start | Erfassung starten |
flow_edge_opc_stop | POST | /api/v1/opc/{opcId}/stop | Erfassung stoppen |
1.2 Tag
| Slug | Method | URL | Beschreibung |
|---|---|---|---|
flow_edge_tag_list | GET | /api/v1/opc/{opcId}/tag | Tag-Liste eines bestimmten OPC (inkl. letztem Wert/Status) |
flow_edge_tag_create | POST | /api/v1/opc/{opcId}/tag | Einzelnen Tag anlegen |
flow_edge_tag_update | PUT | /api/v1/tag/{tagId} | Einzelnen Tag teilweise aktualisieren (Merge mit bestehender Row, dann Upsert) |
flow_edge_tag_delete | DELETE | /api/v1/tag/{tagId} | Einzelnen Tag löschen |
flow_edge_tag_read | GET | /api/v1/tag/{tagId}/value[?fresh=true] | Cache-Wert (default), bei fresh=true direkter Read von der PLC |
flow_edge_tag_write | POST | /api/v1/tag/{tagId}/value | Wert schreiben — alle Protokolle unterstützt (HTTP / OPC-UA / Modbus / MELSEC / S7 / LS / EIP) |
1.3 Monitoring / Edge / Transfer
| Slug | Method | URL | Beschreibung |
|---|---|---|---|
flow_edge_monitoring | GET | /api/v1/monitoring | Alle Felder von MonitorBean wie CPU/Speicher/Netzwerk/Festplatte/Threads |
flow_edge_info | GET | /api/v1/edge | Metadaten zu Edge-Identifikation/Version/Standort/OS/Betriebszeit |
flow_edge_transfer | GET | /api/v1/transfer | Health-Status je outbound transfer (API/MQTT/Sparkplug) |
1.3a System (keine Authentifizierung — für LB / k8s-Probe / OTA)
Von /api/v1/system/* passieren die folgenden 3 die Whitelist ApiAuthFilter (es werden keinerlei OPC-/Tag-Informationen offengelegt).
| Method | URL | Beschreibung | HTTP |
|---|---|---|---|
| GET | /api/v1/system/health | 8 Komponenten (cassandra / redis / mqtt / node_red / opc_ua / edge_core / tse / grafana) TCP-Probe mit 200 ms + Gesamtstatus | 200 (all UP) / 503 (DEGRADED) |
| GET | /api/v1/system/ready | Bereitschaftsstatus von monitor + V5 api_client | 200 / 503 |
| GET | /api/v1/system/version | Metadata.VERSION + BUILD_DATE + image_tag aus /etc/kopens/version.env + container_mode aus /.dockerenv | 200 |
Beispiel /health:
{
"data": {
"status": "UP",
"uptime_ms": 1720008,
"components": {
"cassandra": "UP", "redis": "UP", "mqtt": "UP",
"node_red": "UP", "opc_ua": "UP"
}
}
}
Beispiel /version:
{
"data": {
"product_name": "PlantPulse Edge",
"version": "2026",
"build_date": "20260523",
"image_tag": "2026-20260523",
"container_mode": true
},
"meta": {
"timestamp": 1778925572946,
"request_id": "..."
}
}
OTA upgrade.sh entscheidet anhand der /api/health-Probe (300 Sekunden) über ein Auto-Rollback. Auch der Docker HEALTHCHECK verwendet /health.
1.4 OPC-UA Viewer (OPCUAViewerAPI)
Endpoint / Knotenbaum / Zeitreihe des integrierten OPC-UA-Servers — wird vom Bildschirm /ui/opcua verwendet.
| Method | URL | Beschreibung |
|---|---|---|
| GET | /ui/opcua/info[?reveal=true] | Endpoint-URL (TCP/TLS) / Application-Name / Authentifizierungsdaten des integrierten OPC-UA-Servers (reveal=true + bei authentifizierter Session Passwort im Klartext) |
| GET | /ui/opcua/tree | Baum Site → OPC → Tag (inkl. NodeId). Das System-OPC EDGE_* ist immer connection_status=CONNECTED |
| GET | /ui/opcua/history?tagId=...&minutes=N&limit=M | Zeitreihenabfrage aus Cassandra tm_tag_point. Liefert nach Eingabeprüfung ein aufsteigend sortiertes points-Array |
1.5 Upgrade / System (ConfigAPI)
/config/* ist von der v1 REST API getrennt (für die Administrationsoberfläche), wird hier aber der Vollständigkeit halber mit dokumentiert.
| Method | URL | Beschreibung |
|---|---|---|
| GET | /config/upgrade/check | Holt server-to-server die VERSION.JSON von product.kopens.io und liefert {result, latest_version, latest_build_date} zurück. Bei Fehlschlag result=ERROR |
| POST | /config/upgrade | Führt bin/upgrade.sh aus (langlaufender Vorgang) |
| POST | /config/restart | Führt bin/restart.sh aus (Tomcat-Neustart) |
| POST | /config/reboot | Führt bin/reboot.sh aus (OS-Neustart) |
| POST | /config/temp-clean | Führt bin/clean.sh aus (Bereinigung von Logs/temporären Dateien) |
| POST | /config/backup | Führt bin/backup.sh aus |
| POST | /config/firmware | Führt bin/firmware.sh aus (dnf update -y) |
| GET | /config/load | Liefert app.properties als Text |
| POST | /config/save | Speichert app.properties |
2. Beispiele für Request / Response
2.1 GET /api/v1/opc — OPC-Liste
curl -s http://<edge-host>/api/v1/opc | jq
{
"result": "OK",
"data": {
"data": [
{
"opc_id": "OPC_UA_Kepware",
"opc_type": "OPCUA",
"opc_name": "OPC_UA_Kepware",
"opc_agent_ip": "192.168.0.40",
"opc_agent_port": "49320",
"auto_collect": "true",
"timecycle": "1000",
"options": {"username": "kopens", "password": "***", "discovery": "false"},
"tag_count": 11,
"point_count": 5819,
"connection_status": "CONNECTED",
"scan_status": "START"
}
]
}
}
⚠ Form
data.data[](doppeltes Wrapping) — siehe Abschnitt Envelope oben.
2.2 POST /api/v1/opc — OPC + Tags gebündelt anlegen
curl -X POST http://<edge-host>/api/v1/opc \
-H "Content-Type: application/json" \
-d '{
"opc_id": "OPC_NEW",
"opc_type": "OPCUA",
"opc_name": "신규 연결",
"opc_agent_ip": "10.0.0.10",
"opc_agent_port": "49320",
"site_id": "SITE_00001",
"auto_collect": true,
"timecycle": 1000,
"tag_list": [
{"tag_id": "TAG_001", "tag_name": "Sine",
"plc_address": "ns=2;s=Sine1", "data_type": "Float"}
]
}'
2.3 POST /api/v1/opc/{opcId}/tag — Einzelnen Tag anlegen
{
"tag_id": "TAG_NEW",
"tag_name": "신규 태그",
"plc_address": "ns=2;s=NewTag",
"data_type": "Float",
"description": "..."
}
Antwort:
{ "result": "OK", "data": { "result": "SUCCESS", "tag_id": "TAG_NEW" } }
site_id wird automatisch aus dem OPC übernommen.
2.4 PUT /api/v1/tag/{tagId} — Tag teilweise aktualisieren
Im Request-Body werden nur die zu ändernden Felder gesendet. Der Server merged mit der bestehenden Row und führt anschließend ein Upsert aus.
{ "description": "변경된 설명" }
2.5 GET /api/v1/tag/{tagId}/value — Letzten Wert abfragen
Standard (Cache):
curl -s http://<edge-host>/api/v1/tag/TAG_UA_0004/value | jq
{
"result": "OK",
"data": {
"tag_id": "TAG_UA_0004",
"value": "28.9688",
"value_time": "2026-05-06 20:59:39.000",
"value_read_status": "SUCCESS",
"value_read_error_message": ""
}
}
fresh=true (direkter Read von der PLC):
curl -s 'http://<edge-host>/api/v1/tag/TAG_UA_0004/value?fresh=true' | jq
Bei fehlgeschlagenem PLC-Read äußeres result=ERROR + message:
{ "result": "ERROR", "message": "fresh read failed: ..." }
⚠ Auch die Abfrage eines nicht existierenden Tags antwortet mit Envelope
result=OKund liefert eine Platzhalter-Payload zurück, in dervaluemit"-"gefüllt ist (aktuelles Verhalten).
2.6 POST /api/v1/tag/{tagId}/value — Wert schreiben
curl -X POST http://<edge-host>/api/v1/tag/TAG_HTTP_001/value \
-H "Content-Type: application/json" \
-d '{ "value": "42" }'
Erfolg:
{
"result": "OK",
"data": {
"result": "SUCCESS",
"opc_id": "OPC_LS_TEST",
"plc_address": "D1000",
"value": "42",
"actual_read": "42"
}
}
Fehlschlag (Treiber liefert false / OPC DISCONNECTED / Scheduler nicht aktiv) — äußeres Envelope result=ERROR:
{ "result": "ERROR", "message": "쓰기 실패 (opc_type=EIP) — EIP 펌웨어/패치에 따라 ..." }
Unterstützte Protokolle: HTTP / OPC-UA / Modbus / MELSEC / S7 / LS XGT / EIP — alle möglich. Bei EIP sind je nach PLC-Firmware/Patch Einschränkungen möglich.
2.7 POST /api/v1/opc/{opcId}/start — Erfassung starten
curl -X POST http://<edge-host>/api/v1/opc/OPC_NEW/start
Ein automatischer Start der integrierten OPCUA-/MODBUS-Simulatoren wird nicht unterstützt. Verwenden Sie für Test-/Demodaten externe plantpulse-simulator oder Test-Tags.
2.8 GET /api/v1/edge — Edge-Identifikation / Betriebsinformationen
Felder, die EdgeAPI.edgeInfo() direkt befüllt:
{
"result": "OK",
"data": {
"id": "EDGE_00303",
"site_id": "SITE_00001",
"site_name": "S1_LOTTE_CS_DJ_SITE",
"hostname": "EDGE-303",
"product_name": "PlantPulse Edge",
"version": "2026",
"build_date": "2026-05-08",
"os_name": "Linux",
"os_version": "6.14.5-100.fc40.x86_64",
"os_arch": "amd64",
"started": true,
"started_date": 1778066700000,
"uptime_ms": 124500,
"always_on": false,
"ttl": 30,
"sended_count": 8063
}
}
2.9 GET /api/v1/monitoring — Systemmetriken
Serialisiert alle Felder von MonitorBean unverändert. Aktualisierung im Sekundentakt.
curl -s http://<edge-host>/api/v1/monitoring | jq '.data | keys'
Wichtige Felder: cpu_used_percent, memory_used_percent, disk_used_percent, temperature, ping, api, opc_count, tag_count, mps, mps_history, queue_size, plc_value_read_success_count, plc_value_read_error_count, plc_value_write_success_count, plc_value_write_error_count, plc_con_connected_count, plc_con_disconnected_count, plc_scan_start_count, plc_scan_not_collect_count, plc_scan_stop_count, sended_point_count, sended_point_bytes, system_total_db_size, system_error_count, docker_on, docker_container_up_count, docker_container_total_count.
2.10 GET /api/v1/transfer — Health der externen Übertragung
curl -s http://<edge-host>/api/v1/transfer | jq
{
"result": "OK",
"data": {
"transfers": [
{ "type": "API", "enabled": true, "connected": true, "sent_count": 8063 },
{ "type": "MQTT", "enabled": true, "connected": true, "sent_count": 12345 },
{ "type": "Sparkplug", "enabled": true, "connected": true,
"group_id": "Plant1", "edge_node_id": "EDGE_00303",
"bdSeq": 7, "seq": 211 }
]
}
}
Gibt das getStatus()-Ergebnis jedes transfer unverändert als Array zurück. Tritt während des Aufrufs eine Exception auf, erfolgt ein Fallback auf {type, status_error}.
2.11 GET /ui/opcua/info — Informationen zum integrierten OPC-UA-Server
curl -s http://<edge-host>/ui/opcua/info | jq
{
"result": "OK",
"data": {
"application_name": "PlantPulse Edge OPC-UA Server",
"product_uri": "urn:plantpulse:opcua:server",
"domain": "127.0.0.1",
"tcp_endpoint": "opc.tcp://127.0.0.1:12000",
"tls_endpoint": "opc.tcp://127.0.0.1:12443",
"namespace_index": 2,
"auth_username": "edge",
"auth_password_set": true,
"auth_anonymous": false
}
}
Bei ?reveal=true und authentifizierter Session wird auth_password zusätzlich im Klartext ausgegeben.
2.12 GET /ui/opcua/tree — Knotenbaum
Dreistufiger Baum Site → OPC → Tag. Jeder Tag enthält NodeId, aktuellen Wert, Datentyp und description. Für EDGE-System-OPCs wird connection_status=CONNECTED erzwungen.
NodeId-Konvention: ns=2;s=<SITE>.<OPC>.<TAG>.
2.13 GET /ui/opcua/history — Zeitreihe
| Parameter | Standard | Grenzwert |
|---|---|---|
tagId | erforderlich | max. 200 Zeichen |
minutes | 10 | 1 – 1440 (24 h) |
limit | 600 | 1 – 5000 |
curl -s 'http://<edge-host>/ui/opcua/history?tagId=TAG_S7_1000&minutes=10&limit=100' | jq
{
"result": "OK",
"data": {
"tag_id": "TAG_S7_1000",
"minutes": 10,
"count": 100,
"points": [
{ "ts": 1778176880010, "value": "211", "type": "integer", "quality": 192 }
]
}
}
Fehlt tagId, ist leer oder länger als 200 Zeichen, folgt äußeres result=ERROR (message). Auch wenn der Cassandra-Timestamp als Wrapper-Objekt eintrifft, wird er per extractTimestampMs in Epoch-ms umgewandelt.
3. Muster von Fehlerantworten
| Fall | Antwortform |
|---|---|
| Äußeres Envelope ERROR (Schreibfehler / fehlgeschlagener Fresh-Read / fehlgeschlagene Eingabeprüfung bei opcua/history / fehlgeschlagener Download bei upgrade/check) | {result:"ERROR", message:"..."} (HTTP 200) |
| Internes fachliches ERROR (bei CRUD: tag/opc not found / DB-Integrität / Scheduler nicht aktiv) | {result:"OK", data:{result:"ERROR", msg:"..."}} (HTTP 200) |
| Fehlerhaftes Spring-Mapping (fehlender Pflichtparameter, falsche Methode usw.) | HTTP 500 + Stacktrace — empfohlen: im Controller @RequestParam(required=false) behandeln und in ein Envelope umwandeln |
Authentifizierung — edge.rest.api.auth ist der Installationsstandard true, und ApiAuthFilter verlangt X-API-Key / Bearer (siehe Abschnitt Authentifizierung oben). Nur die Health-Endpunkte sind ausgenommen |
4. Hinweise zum Verhalten
- Abfrage eines einzelnen Tags:
R03vonAPP_TAG.xml(WHERE TAG_ID = :tag_id ALLOW FILTERING). - Nach gebündeltem Anlegen von OPC + Tags / Anlegen eines einzelnen Tags wird
ConnectService.restartComponents()aufgerufen → Reload vonOPCAndTagsCacheund Neustart des OPC-UA-Servers. - Wert schreiben (
tagWriteByTagId): bei HTTPHTTPDriver.bind(), sonstdriver.write(ProtocolAddress). Bei Fehlschlag (ok=false) inklusive hilfreicher Hinweise (z. B. EIP-Firmware-Kompatibilität). Das unmittelbar nach dem Schreiben gelesene Ergebnis wird zusätzlich alsactual_readzurückgegeben (außer bei HTTP). - Transfer-Health (
/api/v1/transfer): sammeltgetStatus()aller inTransferRegistryregistrierten outbound-Komponenten. Die Sparkplug-B-Karte (/ui/main) ruft den Endpunkt alle 5 Sekunden auf. - Der OPC-UA Viewer (
/ui/opcua/*) pollt im internen Netz (2 Sekunden) — nutzt nurOPCAndTagsCache.getInstance()+LastValueMap.getInstance()und erzeugt daher geringe Last. - Legacy, deprecated:
/api/http(APIController) — Verwendung von/api/v1/tag/{tagId}/valueempfohlen.
5. Codepfade
- Controller:
src/plantpulse/app/edge/api/v1/OpcAPI.java · api/v1/TagAPI.java - OPC-UA Viewer:
src/plantpulse/app/edge/module/opcua/OPCUAViewerController.java - Upgrade-Proxy:
src/plantpulse/app/edge/module/system/config/ConfigController.java - Geschäftslogik:
src/plantpulse/app/edge/module/connect/ConnectService.java - Helper für Antwort-Envelope:
plantpulse.app.edge.core.web.ControllerSupport(ok()/error())