Repository und Entwicklungsumgebung

Zweck

Diese Seite beantwortet die Fragen, die sich beim ersten Öffnen des Repositories stellen: Was liegt hier, wie bekomme ich es zum Laufen, wie lasse ich die Tests laufen, und welche Einstellung gehört in eine Umgebungsvariable statt in die Datenbank. Der innere Aufbau des Adapters steht in Architektur im Detail, die Abläufe des Betriebs in Betriebsanleitungen.

Was im Repository liegt

Pfad Inhalt

src/thingsboard_frost_adapter/

Der Adapter selbst — ein FastAPI‑Dienst, in vier Schichten gegliedert (Architektur im Detail: Modulschichten).

src/frontend/

Die Streamlit‑Verwaltungsoberfläche. Eine eigenständige Anwendung mit eigenem Container und eigenem Port (Frontend (Streamlit)).

tests/

unit/ und integration/, getrennt nach Abhängigkeit und nicht nach Umfang (siehe Tests).

docs/

Diese Dokumentation als Antora‑Komponente mit den beiden Modulen tech_doc und user_doc.

rulechain/

thingsboard_frost_adapter.json — die exportierte ThingsBoard Rule Chain, die Telemetrie an den Adapter schickt. Wird je Mandant von Hand importiert und angepasst (Rule Chain je Mandant verteilen).

initdb/

20-frost-b.sql — legt die Datenbank des zweiten FROST‑Servers an. Wird nur beim Erstellen des Volumes ausgeführt (siehe Die Entwicklungsumgebung).

keycloak-imports/

Der Realm‑Export, den Keycloak beim Start mit --import-realm einliest.

docker-compose.yml
docker-compose.app.yml

Infrastruktur bzw. Anwendung — zwei Dateien mit einem gemeinsamen Projektnamen (siehe Die Entwicklungsumgebung).

Dockerfile

Ein mehrstufiger Build mit den Zielen backend und frontend (siehe Container‑Images).

.env.example, .streamlit/secrets-example.toml

Vorlagen für die beiden Konfigurationsdateien, die nicht eingecheckt sind (siehe Konfiguration).

Unterhalb von src/ liegen damit zwei Pakete nebeneinander: der Adapter und das Frontend. Sie teilen sich das Poetry‑Projekt und eine Abhängigkeitsliste, laufen aber als zwei getrennte Prozesse und sprechen ausschließlich über die REST‑API des Adapters miteinander.

Die Entwicklungsumgebung

Die Umgebung ist auf zwei Compose‑Dateien verteilt, die denselben Projektnamen (name: frost_allinone) und damit dasselbe Netz benutzen:

Datei Dienste

docker-compose.yml

Die Infrastruktur: db_adapter (Postgres des Adapters, 5432), database (PostGIS für die FROST‑Server), frost_server (8080), frost_server_b (8081), keycloak_web (8180), db_keycloak, redis (6379), thingsboard (9090). Die Testmandanten darin legt die Testsuite an, kein eigener Dienst — siehe Das lokale ThingsBoard.

docker-compose.app.yml

Die Anwendung: thingsboard_frost_adapter (8000) und frontend (8501).

Für die tägliche Arbeit reicht die erste Datei — Adapter und Frontend laufen dann lokal aus dem Poetry‑Environment gegen die Container:

docker compose -f docker-compose.yml up

Zwei FROST‑Server sind Absicht

Die Entwicklungsumgebung enthält zwei FROST‑Instanzen, weil die Integrationstests die Mandantentrennung prüfen und dafür zwei Mandanten brauchen. frost_server_b ist bewusst dauerhaft und nicht optional: Ohne CI‑Job ist ein Teststapel, den man erst einschalten muss, ein Teststapel, den nichts jemals startet. Die Mehrkosten sind ein Container — er teilt sich den Datenbankserver (als zweite Datenbank) und den Keycloak‑Realm mit frost_server und braucht dort nur einen eigenen Client.

Beide Instanzen laufen auf demselben festgeschriebenen Image‑Tag (FROST_SERVER_TAG), damit sie nicht unbemerkt auf verschiedene FROST‑Versionen auseinanderlaufen (Image‑Tag).

Beim Hinzufügen eines FROST‑Servers muss das Volume neu erstellt werden. Die Datei initdb/20-frost-b.sql legt die Datenbank des zweiten Servers an, und Init‑Dateien führt Postgres ausschließlich dann aus, wenn das Datenverzeichnis leer ist. Eine später hinzugefügte Datenbank entsteht deshalb erst nach:

docker compose down -v
docker compose -f docker-compose.yml up

Die Datei ist zudem als einzelne Datei eingebunden und nie als Verzeichnis: Das Image bringt in demselben Verzeichnis sein eigenes 10_postgis.sh mit, das ein Verzeichnis‑Mount verdecken würde — womit auch der heute funktionierende FROST seine PostGIS‑Erweiterung verlöre.

Das lokale ThingsBoard

Der Stapel enthält eine eigene ThingsBoard‑Installation (thingsboard, Port 9090) — die Community Edition, weil es die Professional Edition nicht als frei beziehbares Image gibt. Genau das ist auch der Grund, aus dem es THINGSBOARD_EDITION überhaupt gibt: Die Community Edition kennt keine API‑Key‑Authentifizierung, der lokale Stapel läuft deshalb mit ce und Benutzername/Passwort.

Der Image‑Tag (THINGSBOARD_CE_TAG) ist aus einem härteren Grund festgeschrieben als bei FROST: ThingsBoard legt sein Schema einmal an, nämlich auf einem leeren Volume, und migriert beim Start nicht. Ein wandernder Tag lässt deshalb früher oder später ein neueres Image auf ein älteres Schema treffen — ein Fehler, der sich nicht von selbst meldet. Das Volume thingsboard_data bleibt erhalten, weil der erste Start die Schema‑Installation mitmacht und gut eine Minute dauert, während jeder weitere in Sekunden bereit ist.

docker compose up hinterlässt eine leere ThingsBoard‑Installation; die beiden Testmandanten legt die Testsuite an. Das Fixture thingsboard_provisioned in tests/integration/conftest.py ruft dazu einmal je Lauf provision() aus tests/integration/thingsboard_data.py auf. Ein eigener Bereitstellungs‑Container dafür wäre eine zweite Stelle, die dasselbe entscheidet: Die Bereitstellung ist ohnehin aus jedem Zustand heraus wiederholbar — Vorhandenes bleibt unangetastet, Fehlendes entsteht neu, und ein Mandantenadmin, dessen Passwort nicht mehr das aus .env ist, wird samt Mandant neu aufgebaut, weil ThingsBoard keinen Weg bietet, ein Passwort von der Sysadmin‑Seite aus zu setzen. Die Zugangsdaten in .env sind damit verbindlich und nicht bloß erhofft.

Der Healthcheck meldet sich als Sysadmin an — er tut also genau das, was die Bereitstellung als Erstes tut, und docker compose up -d --wait im Session‑Fixture macht daraus die Schranke, hinter der der Lauf beginnen darf. Alles Billigere wurde gemessen und lügt: Auf einem leeren Volume beantwortet die Installationsphase rund 35 Sekunden lang HTTP‑Anfragen, GET /login mit 200, ohne dass die API existiert; danach ist der Server für weitere rund 30 Sekunden ganz weg, während ThingsBoard in den eigentlichen Betrieb neu startet. Ein Port‑ oder /login‑Test meldete „bereit" deshalb zweimal falsch, und der erste Test liefe auf jedem frischen Rechner hinein — was wie eine unzuverlässige Testsuite aussähe und keine ist.

Der veröffentlichte Port 8000

Die Portfreigabe 8000:8000 des Dienstes thingsboard_frost_adapter in docker-compose.app.yml ist eine reine Entwicklungshilfe und darf in der echten Installation nicht vorhanden sein. Der Adapter authentifiziert niemanden; die Unerreichbarkeit über das Netz ist die Sicherheitsgrenze. Die Begründung und die Folgen stehen in Unerreichbarkeit ist die Sicherheitsgrenze.

Lokal starten

Abhängigkeiten installieren — das installiert auch das Projekt selbst und nicht nur seine Abhängigkeiten, was den src‑Layout‑Import überhaupt erst auflöst:

poetry install

Backend:

poetry run uvicorn thingsboard_frost_adapter.main:app --reload

Frontend — setzt ein laufendes Backend voraus:

poetry run streamlit run src/frontend/main.py

Beim Start legt bootstrap() das Schema an und füllt die Stammdaten. Es spricht dabei kein externes System an und prüft insbesondere keinen Mandanten (Architekturüberblick).

Tests

Die oberste Achse von tests/ ist die Abhängigkeit, nicht der Umfang.

Baum Voraussetzung

tests/unit/

Nichts. Läuft aus einem blanken Checkout in deutlich unter einer Sekunde und bleibt damit brauchbar, während der Compose‑Stapel unten ist.

tests/integration/

Der Compose‑Stapel, und sonst nichts. Das ThingsBoard, gegen das sie laufen, steht mit darin (Das lokale ThingsBoard); kein Test erreicht ein System außerhalb.

-m staging

Zusätzlich Netzzugang zum gemeinsamen Staging‑System und ein Lauf, der von außen auf pe konfiguriert ist — siehe Der Staging‑Lauf. Standardmäßig abgewählt.

poetry run pytest                    # alles, was die aktuelle Konfiguration erreichen kann
poetry run pytest -m integration     # nur die Integrationstests, nur gegen den Stapel

Kein Test setzt eine Umgebungsvariable und keiner verbiegt config. Gegen welches ThingsBoard die Suite läuft und mit welcher Edition, steht in .env und wird von außen gesetzt — es sind prozessweite Tatsachen, und ein Test, der sie verschöbe, würde über ein Deployment urteilen, bei dem sich die ihm gereichten Mandantenzeilen gar nicht anmelden könnten. Aus demselben Grund ist der Staging‑Lauf ein zweiter Aufruf und keine Verzweigung im Code.

--teardown führt am Ende docker compose down -v aus, und das -v nimmt das ThingsBoard‑Volume mit. Der nächste Lauf zahlt dann die Kaltstart‑Installation des Schemas: rund zwei Minuten, bevor der erste Test beginnt. Das ist kein Hänger. Ohne die Option wird nichts zerstört und eine warme Instanz ist in Sekunden bereit.

Der Staging‑Lauf

Zwei Tests, beide reiner Gutfall: Ein Gerät entsteht in FROST aus seinem ThingsBoard‑Datensatz, und Telemetrie landet als Observation an ihrem Datastream. Mehr nicht — alles andere ist lokal bewiesen, und es hier zu wiederholen kostete nur Schreibzugriffe auf ein System, das andere gerade benutzen.

Es ist die einzige Deckung, die der pe‑Zweig noch hat: Die Community Edition kennt keine API‑Key‑Authentifizierung, also sind Anmeldeverfahren, Client‑Klasse und Credential‑Form dort von der ersten bis zur letzten Zeile ein anderer Weg — und das Deployment, gegen das dieser Adapter tatsächlich läuft, ist PE.

Vorlage ist .env.staging.example; die ausgefüllte .env.staging steht in .gitignore.

set -a; source .env.staging; set +a
poetry run pytest -m staging

set -a exportiert, was die Datei setzt, und eine exportierte Variable gewinnt gegen .env: Der Adapter lädt diese mit load_dotenv(), das Vorhandenes nie überschreibt. Die lokalen Vorgaben in .env bleiben also unangetastet. Der Compose‑Stapel muss trotzdem laufen — die FROST‑Server, die diese Tests lesen, sind die lokalen; geprüft wird die ThingsBoard‑Hälfte.

Der Lauf schreibt die Registry‑Zeilen der beiden Testmandanten auf PE‑Credentials um: Sie tragen danach einen API‑Key statt Benutzername und Passwort. Das ist kein Schaden, sondern der dokumentierte Editionswechsel — der nächste lokale Lauf registriert sie ebenso selbstverständlich wieder zurück. Eine Zeile mit der Credential‑Form der jeweils anderen Edition wird dabei gelöscht und neu angelegt, weil die Registry jede Änderung an ihr zu Recht verweigert.

Welcher Test wohin gehört, regelt die Spiegelregel in Architektur im Detail; ihr verbindlicher Wortlaut steht im Docstring von tests/unit/init.py.

Konfiguration

Die zentrale Unterscheidung: Eine Umgebungsvariable ist ein Wert, den der Prozess nur einmal halten kann. Alles, was sich je Mandant unterscheidet — die FROST‑Server‑URL, deren Keycloak‑Client, die ThingsBoard‑Zugangsdaten — steht in der Tabelle tenant und nicht in der Konfiguration. Die ThingsBoard‑Edition ist genau deshalb Konfiguration: Sie ist eine Eigenschaft der einen Installation hinter THINGSBOARD_URL, nicht der einzelnen Mandantenzeile.

Der Keycloak‑Zugang ist die Ausnahme, die die Regel bestätigt: Es gibt einen globalen Adapter‑Benutzer, dem die Client‑Rollen des FROST‑Clients jedes Mandanten zugewiesen sind. Deshalb sind Realm, Benutzername und Passwort deployment‑weit, und nur keycloak_client_id ist mandantenspezifisch.

.env — der Adapter

Variable Bedeutung

EXTERNAL_IP

Die von den Containern aus erreichbare Adresse des Hosts. Fehlt KEYCLOAK_URL, wird daraus http://<EXTERNAL_IP>:8180/ gebildet.

KEYCLOAK_REALM_NAME_FROST_SERVER
KEYCLOAK_USERNAME_FROST_SERVER
KEYCLOAK_PASSWORD_FROST_SERVER

Realm und Zugangsdaten des einen globalen Adapter‑Benutzers.

THINGSBOARD_URL

Die eine ThingsBoard‑Installation. Ein Deployment, viele ThingsBoard‑Mandanten — welcher davon gelesen wird, entscheiden die Zugangsdaten aus der Mandantenzeile. Lokal ist das http://localhost:9090, also das ThingsBoard aus dem Stapel; der Bereitstellungs‑Container bekommt denselben Namen auf den Dienstnamen gesetzt, weil localhost dort er selbst wäre.

THINGSBOARD_EDITION

Welche ThingsBoard‑Edition hinter THINGSBOARD_URL steht: pe (Standard) oder ce. Sie entscheidet, womit sich jede Mandantenzeile anmeldet — pe mit einem API‑Key, ce mit Benutzername und Passwort, weil die Community Edition keine API‑Key‑Authentifizierung kennt. Konfiguriert wird die Edition und nicht das Anmeldeverfahren: So lässt sich die Kombination, die nicht funktionieren kann (ce plus API‑Key), gar nicht erst hinschreiben. Zum Wechsel siehe Wechsel der ThingsBoard‑Edition.

TENANT_SECRET_KEY

Der Fernet‑Schlüssel, mit dem jedes ThingsBoard‑Geheimnis eines Mandanten — API‑Key unter PE, Passwort unter CE — in der Datenbank verschlüsselt ist. Pflicht, ohne Standardwert und ohne Klartext‑Rückfallweg, damit die Verschlüsselung sich nicht still abschalten kann. Zum Verlustfall siehe Verlust des Verschlüsselungsschlüssels.

THINGSBOARD_FROST_ADAPTER_DB_*

Name, Benutzer, Passwort, Host und Port der Adapter‑Datenbank.

REDIS_URL, LOG_LEVEL

Redis‑Verbindung für die verteilten Sperren; Log‑Stufe.

FROST_SERVER_TAG

Das Image‑Tag aller FROST‑Instanzen. Niemals latest.

THINGSBOARD_CE_TAG

Das Image‑Tag des lokalen ThingsBoard. Niemals latest, und zwar zwingender als bei FROST — siehe Das lokale ThingsBoard.

THINGSBOARD_TEST_USERNAME_A
THINGSBOARD_TEST_PASSWORD_A
THINGSBOARD_TEST_USERNAME_B
THINGSBOARD_TEST_PASSWORD_B

Nur für die Tests: die Mandantenadmins der beiden Mandanten im lokalen ThingsBoard. Sie sind nirgends nachzuschlagen — die Bereitstellung legt die Benutzer mit genau diesen Werten an (siehe Das lokale ThingsBoard).

.env.staging — nur der Staging‑Lauf

Wird ausschließlich für pytest -m staging in die Shell geladen und sonst von nichts gelesen (Der Staging‑Lauf). Sie nennt THINGSBOARD_URL und THINGSBOARD_EDITION="pe" für das gemeinsame System sowie THINGSBOARD_TEST_API_KEY_A und THINGSBOARD_TEST_API_KEY_B — je einen schreibberechtigten Schlüssel aus zwei verschiedenen ThingsBoard‑Mandanten. Landen beide im selben Mandanten, verweigert die Suite den Dienst: Jede Isolationseigenschaft, die sie prüft, wäre dann nicht mehr widerlegbar. Der Adapter selbst liest die Schlüssel nie.

Einen neuen Schlüssel erzeugt:

python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

.streamlit/secrets.toml — das Frontend

Das Frontend liest seine eigene Datei, nicht .env. Entscheidend ist frost_adapter_backend_base_url — lokal http://localhost:8000, im Container http://thingsboard_frost_adapter:8000 — sowie der [auth]‑Block für die Keycloak‑Anmeldung. Vorlage ist .streamlit/secrets-example.toml; die Details stehen in Frontend (Streamlit).

Weder .env noch .streamlit/secrets.toml sind eingecheckt; beide stehen in .gitignore. Eingecheckt sind nur die beiden Beispieldateien.

Container‑Images

Der Dockerfile ist mehrstufig und endet in drei Zielen, die sich eine gemeinsame runtime‑Stufe teilen:

Ziel Ergebnis

backend

uvicorn thingsboard_frost_adapter.main:app auf Port 8000.

frontend

streamlit run src/frontend/main.py auf Port 8501.

Die CI baut beide getrennt (build_and_publish_backend_docker_image, build_and_publish_frontend_docker_image) und veröffentlicht anschließend diese Dokumentation als GitLab Pages.