In diesem Blog Artikel zeigen wir, wie einfach es ist, das smarte elektronische Schließsystem
Xesar von EVVA mit einem MQTT Listener zu verbinden und auf Events im System zu reagieren.


Wir verwenden dafür die Open-Source Bibliothek Xesar-Connect, deren Entwicklung von der
open200 unterstützt wird. Xesar-Connect ermöglicht es, einfach und komfortabel die asynchrone
Schnittstelle von Xesar zu nutzen.


Xesar Event Bus


Das elektronische Schließsystem Xesar von EVVA bietet eine MQTT-Schnittstelle, welche es
ermöglicht, auf verschiedene Events im System zu reagieren. Dazu zählen unter
anderem Hinweise auf leer werdende Batterien, offene Wartungsaufträge, und vieles mehr.

In diesem Blog Artikel wollen wir einen einfachen MQTT Listener implementieren, der das Event
“Battery Empty” behandelt und den Namen des betroffenen Einbauortes bzw. EVVA
Komponente ausgibt.


Tech Stack

  • JDK 21
  • Kotlin 1.9
  • Spring Boot 3.2
  • Xesar-Connect 1.0


Wir nutzen Kotlin und Spring Boot für die Implementierung des MQTT Listeners.
Xesar-Connect ermöglicht einen einfachen und effizienten Zugriff auf die Xesar asynchrone
MQTT Schnittstelle mit Hilfe von Kotlin Coroutinen.


Xesar-Connect ist auch mit älteren JDK Versionen kompatibel, wie zum Beispiel JDK 11.
Allerdings empfehlen wir die Verwendung von JDK 21 oder neuer für die neuesten Features und
Verbesserungen.


Account in Xesar anlegen


Damit wir Zugriff auf die Xesar MQTT Schnittstelle erhalten, benötigen wir einen Benutzer in der
Xesar Anlage. Wir erstellen daher einen eigenen Benutzer den wir nur für die Integration nutzen
mit einem generierten Passwort. Wir empfehlen in Produktivumgebungen die Verwendung von
starken Passwörtern und das nutzen von Benutzergruppen mit minimalen Rechten.

Xesar Connect - Anlegen eines Users

Anlegen eines users in Xesar connect


Nun können wir die “Konfiguration” des Benutzers als ZIP-Datei herunterladen. In der
Konfiguration sind die notwendigen Daten für die Verbindung und Authentifizierung enthalten.

Xesar Connect Maske für das Herunterladen der Konfiguration

Herrunterladen der Xesar Konfiguration


Projekt setup


Wir erstellen ein neues Spring Boot Projekt mit dem Spring Initializr und wählen als Sprache
Kotlin aus. Für dieses Projekt benutzen wir Gradle mit Kotlin DSL als Build-Tool und die neueste
Spring Boot 3.2 Version mit JDK 21.
Nachdem wir das Projekt erstellt haben, fügen wir noch weitere Bibliotheken als Abhängigkeiten
hinzu:

implementation("com.open200:xesar-connect:1.0.0")
implementation("org.bouncycastle:bcprov-jdk18on:1.78")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")


Wir benötigen Bouncy Castle als Security Provider, da Xesar-Connect auf die kryptografischen
Funktionen von Bouncy Castle zurückgreift um eine mTLS Verbindung mit Xesar’s MQTT Broker
aufzubauen.


Die Kotlin Coroutines Bibliotheken sind notwendig, um asynchrone Operationen von XesarConnect zu ermöglichen.


Wir löschen die application.properties Datei und erstellen stattdessen eine neue Datei application.yml im resources Ordner. Wir fügen in der Datei nun den Pfad zum heruntergeladenen Konfigurations-ZIP als property xesarconnect.credentials-zip-path hinzu, damit wir später darauf zugreifen können:

xesarconnect:
  credentials-zip-path: <path-to-user-config.zip>


Hello Xesar World


Um die Verbindung zur Xesar Anlage herzustellen, muss Xesar-Connect initialisiert werden.
Dafür erstellen wir eine Spring Configuration-Klasse XesarConnectConfig und verbinden eine
Xesar-Connect Instanz mit der Xesar Anlage:

@Configuration
class XesarConnectConfig {

    init {
        Security.addProvider(BouncyCastleProvider())
    }
    @Bean
    fun config(@Value("\${xesarconnect.credentials-zip-path}") zipPath: Path): Config {
        return Config.configureFromZip(configurationZipFile = zipPath, logoutOnClose false)
    }
    @Bean
    fun xesarConnect(config: Config): XesarConnect {
        return runBlocking {
            XesarConnect.connectAndLoginAsync(config, closeUponCoroutineCompletion = false).await()
        }
     }
}

 

Die Klasse bietet die Xesar-Connect Konfiguration in einer eigenen Bean vom Typ Config an. In
der dazu gehörigen Methode config wird der zuvor festgelegte Pfad zur Konfigurations-ZIPDatei genutzt, um mit Hilfe von Config.configureFromZip die Konfiguration zu laden und
zurückzugeben. Xesar-Connect konfiguration könnte auch direkt über die Config.configure
Methode erfolgen, allerdings ist die Verwendung über die ZIP-Datei einfacher.


Die Methode XesarConnect nutzt die Config Bean um eine Verbindung zur Xesar Anlage
aufzubauen und gibt ein XesarConnect Objekt zurück. Da der Verbindungsaufbau asynchron
erfolgt, wird die Methode mit runBlocking ausgeführt und das Ergebnis mit await
abgewartet.

Die zusätzlich übergebene Option closeUponCoroutineCompletion = false
verhindert, dass die Verbindung nach Abschluss der Coroutine wieder geschlossen wird. Dies ist
notwendig, da wir die Verbindung für die gesamte Laufzeit der Anwendung offen halten wollen.


Bevor wir loslegen mit dem eigentlichen Event Listener, testen wir den Verbindungsaufbau und
schreiben einen einfachen Logging Listener, der die Topics aller Events in der Xesar Anlage
mitloggt:

@Component
class LoggingListener(private val xesar: XesarConnect) {

    private val log = LoggerFactory.getLogger(LoggingListener::class.java)

    @EventListener(ApplicationReadyEvent::class)
    suspend fun logTopics() {
        // only subscribe to all topics for testing purposes
        xesar.subscribeAsync(Topics(Topics.ALL_TOPICS)).await()
        xesar.on(AllTopicsFilter()) { log.info("Received message on {}", it.topic) }
    }
}

 


Wir nutzen Spring’s ApplicationReadyEvent um den Logging Listener nach dem Start der
Anwendung zu initialisieren. Die asynchrone Methode logTopics nutzt direkt die
XesarConnect Instanz, welche bereits eine aktive Verbindung zur Xesar Anlage besitzt.

Mit subscribeAsync abonnieren wir alle Topics, auf denen in der Xesar Anlage Events publiziert
werden. Xesar-Connect erstellt in dabei im Hintergrund eine MQTT Subscription. Damit wir auf
die Events reagieren können, benötigen wir zusätzlich einen Event Listener.

Dieser wird mit der Methode on registriert und bekommt einen Filter übergeben, der bestimmt, auf welche Events
reagiert werden soll. In diesem Fall reagieren wir auf alle Events, indem wir den
AllTopicsFilter verwenden. Der Event Listener loggt den Topic des empfangenen Events.


Wenn man nun die Anwendung mit ./gradlew bootRun startet und Änderungen an der Xesar
Anlage vornimmt, werden die Events in der Konsole geloggt. Wenn man beispielsweise eine
neue Person in der Anlage erstellt, wir das Topic xs3/1/ces/PersonCreated geloggt.


Battery Empty Event


Nachdem wir nun erfolgreich eine Verbindung zur Xesar Anlage herstellen konnten und auch
bereits Events empfangen können, wollen wir nun einen weiteren Event Listener erstellen, der auf
das Battery Empty Event reagiert.

Dieses Event wird ausgelöst, wenn die Batterie einer EVVA Komponente (zum Beispiel eines EVVA Zylinders) gewechselt werden sollte um einen reibungslosen Betrieb zu gewährleisten.

Da batteriebetriebene Komponenten, wie der EVVA Zylinder, keine direkte Verbindung zum Netzwerk haben, ist zu beachten, dass das Battery Empty Event verzögert auf dem MQTT Broker publiziert wird.

Beispielsweise wird das Event erst publiziert, wenn Zutritte mit dem Zylinder erfolgen und das Event über die Zutrittsmedien an die Anlage übermittelt wird.


Wir erstellen dafür eine neue Klasse BatteryEmptyListener:

@Component
class BatteryEmptyListener(
    private val xesar: XesarConnect,
) {
    private val log = LoggerFactory.getLogger(LoggingListener::class.java)
    private val scope = CoroutineScope(Job() + Dispatchers.IO)
    private val batteryEmpty = Topics.Event.accessProtocolEventTopic(
            GroupOfEvent.MaintenanceComponent,
            EventType.BATTERY_EMPTY)

    @EventListener(ApplicationReadyEvent::class)
    suspend fun listenOnBatteryEmpty() {
        xesar.subscribeAsync(Topics(batteryEmpty)).await()

        xesar.on(TopicFilter(batteryEmpty)) {
            scope.launch {
                val event = AccessProtocolEvent.decode(it.message)
                log.warn(
                    "Battery warning received from xesar on installation point: {} ({})",
                    event.installationPointIdentifier,
                    event.installationPointName)
            }
        }    
    }   
}


Wie zuvor beim Logging Listener nutzen wir Spring’s ApplicationReadyEvent um die
Listener Komponente nach dem Start der Anwendung zu initialisieren. Der Event Listener
abonniert nun das Topic für das Battery Empty Event.

Xesar-Connect bietet Hilfsmethoden wie hier Topics.Event.accessProtocolEventTopic um Topics für spezifische Events zu erstellen.

Obwohl der Logging Listener bereits alle Topics abonniert hat, abonnieren wir hier
explizit das Battery Empty Event, um den BatteryEmptyListener unabhängig vom Logging
Listener zu machen.


Der Event Listener reagiert auf das Battery Empty Event, indem er eine Coroutine startet, die das
empfangene Event deserialisiert und den Namen und die ID des betroffenen Einbauortes bzw.
der betroffenen EVVA Komponente loggt.
Xesar-Connect bietet Klassen wie AccessProtocolEvent um einen einfachen und typisierten
Zugriff auf Eventdaten zu ermöglichen.


Xesar Connect ist einfach zu verwenden


Wir haben nun erfolgreich einen Event Listener erstellt, der auf das Battery Empty Event reagiert.
Dieser Event Listener könnte nun um weitere Funktionalitäten erweitert werden, um
beispielsweise eine Benachrichtigung zu versenden oder eine Aktion auszuführen.


Es sollte noch beachtet werden, dass die Anwendung nicht für den produktiven Einsatz geeignet
ist, da sie keine Fehlerbehandlung oder Tests enthält. Für den produktiven Einsatz sollte die
Anwendung entsprechend erweitert werden.


Der Code von diesem Beispielprojekt ist auf GitHub verfügbar: Xesar Battery-Warning-Listener.
Die Xesar Schnittstelle bietet noch viele weitere Möglichkeiten, um auf Events zu reagieren, die
Anlage zu konfigurieren oder Daten abzufragen. Mehr Details dazu finden Sie auf der offiziellen
Schnittstellendokumentation.