Architekturüberblick

Komponenten im Überblick

                    ┌──────────────┐
                    │   Keycloak   │  Authentifizierung für alle Pfeile
                    └──────────────┘

  ┌──────────────┐   Rule Chain    ┌───────────────────────┐
  │  ThingsBoard │ ──────────────► │                       │
  │  (Mandant A) │   REST /ingest  │                       │       ┌──────────────┐
  └──────────────┘                 │                       │ ────► │ FROST-Server │
                                   │  Thingsboard Frost    │       │  (Mandant A) │
  ┌──────────────┐                 │       Adapter         │       └──────────────┘
  │  ThingsBoard │ ──────────────► │                       │       ┌──────────────┐
  │  (Mandant B) │                 │                       │ ────► │ FROST-Server │
  └──────────────┘                 └───────────────────────┘       │  (Mandant B) │
                                     │        │        │           └──────────────┘
  ┌──────────────┐   REST            │        │        │
  │  Frontend    │ ──────────────────┘        │        │
  │ (Streamlit)  │                            ▼        ▼
  └──────────────┘                     ┌───────────┐ ┌───────┐
                                       │ Postgres  │ │ Redis │
                                       │ (Adapter) │ │Sperren│
                                       └───────────┘ └───────┘
Komponente Aufgabe

ThingsBoard

Quelle der Gerätedaten. Eine Rule Chain leitet Telemetrie‑ sowie Geräte‑/Asset‑Änderungsereignisse per REST an den Adapter weiter, sobald ein Gerät der Gruppe publish-to-frost hinzugefügt wurde.

Thingsboard Frost Adapter

Nimmt die Ereignisse aus ThingsBoard entgegen, legt bei Bedarf die zugehörigen SensorThings‑Entitäten im FROST‑Server an bzw. aktualisiert sie, und wendet dabei die Geschäftsregeln auf die Stammdaten an.

Adapter‑Datenbank (PostgreSQL)

Persistiert die lokal verwalteten Stammdaten UnitOfMeasurement, UnitOfMeasurementKey und TelemetryKey, welche die Zuordnung von ThingsBoard‑Telemetrie zu SensorThings‑Begriffen definieren. Dazu die kanonischen ObservedProperty‑Zeilen und die Mandanten‑Registry — alle sechs Tabellen stehen in Datenbankschema des Adapters.

Frontend (Streamlit)

Weboberfläche zur Pflege der Stammdaten über die REST‑API des Adapters (siehe Zuordnungen pflegen: ThingsBoard‑Telemetrie → SensorThings API). Der Aufbau der Anwendung steht in Frontend (Streamlit).

FROST‑Server

Referenzimplementierung der OGC SensorThings API; persistiert Things, Locations, Sensors, ObservedProperties, Datastreams und Observations in einer eigenen PostGIS‑Datenbank (siehe Schnittstellenbeschreibung SensorThings API (FROST‑Server)).

Redis

Wird für verteilte Sperren (redis_lock) verwendet, um beim gleichzeitigen Eintreffen mehrerer Telemetrie‑Nachrichten eines Geräts eine doppelte Anlage derselben Entität in FROST zu verhindern.

Keycloak

Stellt die Authentifizierung sowohl für den Zugriff auf den FROST‑Server als auch für Frontend und Adapter bereit.

Aufbau des Adapters

src/thingsboard_frost_adapter/ ist in vier Schichten gegliedert. Der Pfad eines Moduls sagt, was es tut, bevor man die Datei öffnet:

Schicht Aufgabe

routers/

Die HTTP‑Oberfläche: Pfade, Statuscodes, Response‑Modelle. Eine Route ist ein Service‑Aufruf.

services/

Die Entscheidungen: Geschäftsregeln, Reihenfolgen, Fehlerpolitik – und der Ort, an dem sich zwei Repositories treffen.

repositories/db/, repositories/external/

Genau ein Zugriffsweg auf genau ein System: je Modul eine Postgres‑Tabelle (db/) bzw. ein Fremdsystem (external/: FROST, ThingsBoard).

clients/

Der Aufbau einer Verbindung zu System X für Mandant Y, samt Authentifizierung und deren Zwischenspeicherung.

Importe zeigen ausschließlich nach unten: routers/services/repositories/db/, repositories/external/ und clients/; dazu repositories/external/clients/. Unterhalb von services/ importiert nichts eine Schicht über sich, und ein Repository importiert kein anderes Repository. db/, redis/, model/, exceptions/, configuration/ und bootstrap/ sind keine Schichten, sondern Querschnittspakete und dürfen von überall importiert werden.

Die Regel wird ausschließlich in Prosa durchgesetzt – es gibt kein Lint‑ oder CI‑Werkzeug dafür. Die Begründungen, die Verträge zwischen den Schichten und die beiden benannten Ausnahmen stehen in Architektur im Detail.

Datenfluss einer Telemetrie‑Nachricht

  Rule Chain
      │  POST /ingest/{slug}/telemetry
      ▼
  Slug ──► Mandantenzeile ──► Clients dieses Mandanten
      │
      ▼
  Thing vorhanden?
      │ nein ──► Thing, Location, Sensor, Datastreams anlegen
      │            (Gerätedaten aus ThingsBoard + Zuordnungen aus Postgres)
      ▼ ja
  je Telemetrie-Schlüssel mit Zuordnung ──► Observation im Datastream
  1. ThingsBoard sendet über die Rule Chain eine Telemetrie‑Nachricht (Nutzdaten + Metadaten wie deviceName und Zeitstempel) an den Endpunkt POST /ingest/{slug}/telemetry des Adapters. Der slug ist der in der Tenant‑Registry hinterlegte Bezeichner des Mandanten und bestimmt, in welchen FROST‑Server geschrieben wird; einen mandantenfreien Endpunkt gibt es nicht.

  2. Der Adapter prüft, ob für das Gerät bereits ein Thing in FROST existiert; falls nicht, werden Thing, Location, Sensor und die passenden Datastreams anhand der in ThingsBoard gepflegten Gerätedaten und der lokal verwalteten Telemetrie‑Zuordnungen angelegt.

  3. Für jeden in der Nachricht enthaltenen Telemetrie‑Schlüssel, für den eine Zuordnung existiert, wird eine Observation im zugehörigen Datastream angelegt.

Änderungen oder das Löschen von Geräten bzw. Standort‑Assets in ThingsBoard werden über die Endpunkte POST /ingest/{slug}/device/update, POST /ingest/{slug}/device/delete und POST /ingest/{slug}/asset/update entsprechend in FROST nachgezogen.

Erreichbarkeit eines Mandanten

Ob die Abhängigkeiten eines Mandanten funktionieren, wird nicht persistiert, sondern auf Anfrage in Echtzeit überprüft.

Was geprüft wird

Eine Prüfung besteht aus drei Aufrufen — ein Keycloak‑Token mit dem keycloak_client_id des Mandanten, ein GET auf die FROST‑Service‑Root und eine authentifizierte Abfrage gegen ThingsBoard mit den entschlüsselten Zugangsdaten des Mandanten sowie einer OData‑Abfrage Observations?$orderby=phenomenonTime desc&$top=1 gegen den FROST des Mandanten.

Wer sie auslöst

Diese Prüfung hat drei Aufrufer:

  • POST /tenant und PATCH /tenant/{id} führen sie vor dem Schreiben aus und antworten bei einem Fehlschlag mit 400 unter Nennung der fehlgeschlagenen Prüfung, ohne eine Zeile anzulegen oder zu ändern. Ein fehlerhafter Zugangsdatensatz fällt damit in dem einen Moment auf, in dem ein Mensch auf das Formular schaut und nicht bei der ersten Telemetrie‑Nachricht, deren Fehlschlag die Rule Chain still verwirft. Die Folge ist: ein Mandant lässt sich nicht registrieren, solange sein FROST kurzzeitig nicht erreichbar ist.

  • GET /tenant/{id}/health und GET /tenant/health liefern das Ergebnis als TenantHealth.

  • Die Tenants‑Seite des Frontends ruft sie pro Zeile über Test connection bzw. für alle Zeilen über Check all auf. Beim Rendern wird nicht geprüft, um die Ladezeit der Seite nicht zu verlängern.

Beim Start wird nichts geprüft

Der Start des Adapters prüft nichts davon: bootstrap() legt das Schema an, füllt die Stammdaten in Postgres, protokolliert die registrierten Slugs und spricht kein externes System an. Die Registry beim Start durchzugehen, würde den Start von N FROST‑Servern abhängig machen.

Fehler mitten in einer Nachricht

Was die Prüfung nicht sehen kann

Fehler, bei denen die Zugangsdaten gültig bleiben — ein FROST, der den Login akzeptiert, aber ein POST Observations ablehnt, oder ein Gerät, das zwischen Rule Chain und Adapter in ThingsBoard verschwindet — sind für die Prüfung konstruktionsbedingt unsichtbar. Da nichts mitgeschrieben wird, ist das Log der einzige Nachweis, und daher gilt: keine Ausnahme verlässt den Telemetrie‑ oder Gerätepfad ohne eine Logzeile mit Mandanten‑Slug und Entitätsname. Ein nicht erreichbares Zielsystem bleibt dabei ein 502, jeder andere Fehler mitten in der Nachricht wird zu 500.

Alarmiert wird in ThingsBoard

Alarmiert wird in ThingsBoard, nicht im Adapter: die vier TbRestApiCallNode der Rule Chain sind über ihre Failure‑Relation mit je einem TbCreateAlarmNode verbunden, sodass ein 500 oder 502 einen Alarm auf dem auslösenden Gerät im ThingsBoard des jeweiligen Mandanten erzeugt. Der Adapter selbst versendet keine E‑Mails, Webhooks oder Benachrichtigungen — er ist absichtlich von außerhalb des Docker‑Netzes nicht erreichbar.