Hub: Erstellen Sie Ihren Connector

Training

Im Kontext der Gebäudeaufsicht und Automatisierung ist die Lösung Immersive Hub spielt eine zentrale Rolle bei:

  • Sie Zentralisiert alle Daten aus heterogenen Quellen (IoT, SPS, Datenbanken, REST-APIs usw.).
  • Sie Liefert Client-Anwendungen (3D-Beholder, Dashboards, Automatisierungsskripte) aktuelle Informationen in Echtzeit.
  • Es ermöglicht deinen Systemen, Sprich eine gemeinsame Sprache, auch wenn die zugrunde liegenden Protokolle unterschiedlich sind.

Der Hub verfügt über seine Anschlüsse über eine große Anzahl von Protokollen, OPC UA, MQTT, Excel usw.

Aber was, wenn:

  • Dein Sensor oder Ihr Außendienst wird vom Hub nicht nativ unterstützt?
  • Du musst ein verbinden Proprietäre API oder eine benutzerdefinierte Datenquelle ?

Hier kommt die Erstellung eines benutzerdefinierten Steckers ins Spiel.

A Nabenstecker wirkt als ein Brücke zwischen deiner Datenquelle und dem Hub.

  • Es Hört zu oder stellt Fragen Externe Quelle
  • Es überträgt Werte im Hub
  • Es erlaubt Griffe Aufgenommen von Erhalte aktuelle Daten.

In diesem Tutorial werden wir:

  1. Ein konkretes Beispiel aufstellen indem man sich mit einer REST-API verbindet, die Gebäudesensoren simuliert
  2. Erstellen Sie einen Connector, der diese Daten abrufen und dem Hub zugänglich machen kann.

Voraussetzungen

  1. Ich habe einen Hub installiert und gestartet
  2. Kennen Sie die Rolle und Funktionen des Hubs.
  3. Zu wissen, wie man in .Net entwickelt.

Warum einen individuellen Hub-Anschluss erstellen?

In der Lösung Immersiv, Hub ist die Kern der Datenarchitektur. Es fungiert als ein Dirigent zwischen:

  • Datenquellen (IoT-Sensoren, SPS, Datenbanken, REST-APIs usw.)
  • Verbraucher (Beholder 3D, Dashboards, Automatisierungsskripte...)

Der Hub unterstützt bereits Viele Standardprotokolle Zum Beispiel:

  • UCI UA für industrielle Steuerungen
  • Modbus für technische Systeme
  • MQTT für IoT-Flows
  • REST und SQL für traditionelle Dienste und Datenbanken

Der typische Fall: Bereitstellung bei einem Kunden mit einer nicht unterstützten Quellquelle

Beim Einsatz Immersiv in einem EndkundeEs ist nicht ungewöhnlich, sich in einem dieser Szenarien wiederzufinden:

  1. Der Client stellt Daten über ein proprietäres Protokoll bereit
    • Beispiel: Ein Aufzugshersteller mit einem selbstgebautes TCP-Protokoll
    • Oder ein altes BMS-System, das seine Daten an ein Nicht-standardisiertes Format
  2. Der Kunde hat eine spezifische API
    • Beispiel: ein interner REST-Webdienst Aussetzung von Temperaturen, Verbrauch oder Alarmen
    • Oder ein Cloud-Datenfassade Das erfordert eine benutzerdefinierte Authentifizierung
  3. Datenzugriff erfordert eine geschäftliche Anpassung
    • Filtern, aggregieren oder konvertieren der Einheiten, bevor sie nutzbar sind
    • Zusätzliche Logik hinzugefügt, nur die zu melden Aufsichtswerte

In diesen Fällen, Der Hub kann die Quelle derzeit nicht direkt abfragen. Ohne einen dedizierten Connector sind die Daten bleibt unzugänglich für Beholder oder deine Automatisierungsszenarien.

Die Rolle des benutzerdefinierten Steckers

Erstelle eine Individueller Hub-Stecker erlaubt Ihnen:

  • Erweiterung des Hubs um neue Protokolle ohne den Kern der Software zu berühren
  • Maßgeschneiderter Zugriff auf spezifische APIs (REST, SOAP, Dateien, WebSocket-Streams...)
  • Daten transformieren oder filtern bevor es in Immersive erscheint.

In der Praxis:

  • Der Stecker fungiert als Übersetzer zwischen der Kundenquelle und dem Hub
  • Jeder Griff gerettet ist mit einem Pfad verknüpft, den der Connector zu interpretieren kennt
  • The Hub bleibt generisch Während dein Stecker die spezifische Logik übernimmt

Präsentation des Projekts

In diesem Beispielprojekt unterstützen wir ein Eigentumsverwaltungsgesellschaft ("Die ("Die Kunde" für die Zukunft) und die immersive Plattform nutzen wollten, um eine Digitaler Zwilling von seinem Gebäude.

Die IoT-Daten Die für die Echtzeit-Darstellung benötigten Informationen sind auf der Infrastruktur des Kunden verfügbar und über eine interne REST-API verfügbar.

Diese Daten verwenden jedoch eine Spezifisches Protokoll die derzeit von den bestehenden Immersive-Steckverbindern nicht unterstützt werden. Das bedeutet, dass es notwendig sein wird, Entwickeln Sie einen dedizierten Stecker um die Integration und Visualisierung von Informationen im Digital Twin sicherzustellen.

Dieser Schritt ist ein zentrales Thema, um sicherzustellen, dass die Kontinuität zwischen der IoT-Infrastruktur des Kunden sowie die 3D-Darstellungs- und Managementfunktionen, die Immersive bietet.

Detaillierte Präsentation des Projekts (herunterladbar)

Das Projekt zur Simulation der REST-API dieses Miteigentümer-Syndikats ist ein Visual Studio Projekt (C# / ASP.NET Core) das die Infrastruktur repliziert und Daten über eine REST-API bereitstellt. Sie dient als Grundlage für die Entwicklung eines Immersiver Connector angepasst an dieses Protokoll, aber nicht nativ unterstützt.

Klicken Sie hier, um das ASP.Net-Simulationsprojekt herunterzuladen:

Simulierte Geräte und Sensoren

  • Temperatur : "Raum 101 Temperatur" (Einheit: °C).
  • Luftfeuchtigkeit : "Raum 101 Luftfeuchtigkeit" (Einheit: %).
  • Licht : "Flur Lichtniveau" (Einheit: lux).
  • Präsenz : "Büropräsenz" (Boolesch).
  • Energie : "Main Electric Counter" (Einheit: kWh, kumulativ).
  • Rauch : "Rauchmelder" (Boolean).
  • Aufzug : "Lift A" (Multivariable: Boden digital, Zustand enum, Tür Enum).
  • Garagentor : "Garage G1" (multivariabel: Tür enum, Hindernis bool).

Offengelegte REST-Endpunkte

GET  /api/Devices           → Liste de tous les équipements (single-value et multi-variables).
GET  /api/Devices/{id:int}  → Détail d’un équipement par identifiant.
POST /api/Scene             → Déclenche une scène (simulation d’action bâtimentaire).

Datenmodell (JSON-Antworten)

Ein-Wert-Ausrüstung (z. B. "Flurlichtniveau"):

{
  "id": 3,
  "name": "Corridor Light Level",
  "type": "light",
  "path": "",
  "value": "300",
  "unit": "lux",
  "timestampUtc": "2025-08-17T10:23:00Z",
  "variables": null
}

Diese Art von Geräten gibt nur einen Wert über die "Wert".

Mehrvariable Ausrüstung – "Lift A" Aufzug:

{
  "id": 101,
  "name": "Lift A",
  "type": "elevator",
  "path": "/equipments/101",
  "value": null,
  "unit": null,
  "timestampUtc": null,
  "variables": [
    {
      "name": "floor",
      "path": "/equipments/101/floor",
      "kind": "numeric",
      "unit": null,
      "value": "3",
      "timestampUtc": "2025-08-17T10:23:00Z"
    },
    {
      "name": "state",
      "path": "/equipments/101/state",
      "kind": "enum",
      "unit": null,
      "value": "Moving",
      "timestampUtc": "2025-08-17T10:23:00Z"
    },
    {
      "name": "door",
      "path": "/equipments/101/door",
      "kind": "enum",
      "unit": null,
      "value": "Closed",
      "timestampUtc": "2025-08-17T10:23:00Z"
    }
  ]
}

Mehrvariable Ausrüstung – "Garage G1" Garagentor:

{
  "id": 201,
  "name": "Garage G1",
  "type": "garageDoor",
  "path": "/equipments/201",
  "value": null,
  "unit": null,
  "timestampUtc": null,
  "variables": [
    {
      "name": "door",
      "path": "/equipments/201/door",
      "kind": "enum",
      "unit": null,
      "value": "Closed",
      "timestampUtc": "2025-08-17T10:23:00Z"
    },
    {
      "name": "obstacle",
      "path": "/equipments/201/obstacle",
      "kind": "bool",
      "unit": null,
      "value": "false",
      "timestampUtc": "2025-08-17T10:23:00Z"
    }
  ]
}

Diese Ausrüstung setzt mehrere Variablen über die "Variable".

Beispiele für API-Aufrufe

Listen Sie alle Ausrüstungen auf

GET /api/Devices

Ausrüstung durch id abholen

GET /api/Devices/101

Löse eine Szene aus

Unterstützte Szenen: Korridorlicht / Flurlicht (an/aus), Feueralarm / Feueralarm (an/aus).

POST /api/Scene
Content-Type: application/json

{
  "sceneName": "Corridor Light",
  "action": "on"
}

Beispielantwort:

{
  "scene": "Corridor Light",
  "action": "on",
  "timestamp": "2025-08-17T10:23:00Z",
  "message": "💡 Corridor light on"
}

Jetzt, da die Präsentationen mit der Datenquelle des Kunden durchgeführt wurden, können wir anfangen, über die Umsetzung unserer Steckverbinder.

Abstraktion DLL: Zu respektierender Vertrag (IConnector) und Implementierungsbasis (ConnectorBase)

Um einen Stecker zu entwickeln, der vom Hub erkannt wird, muss er Einen Vertrag respektieren Gemeinsam: Dieser Vertrag ermöglicht es dem Hub, den Connector zu identifizieren, seinen Lebenszyklus zu orchestrieren und Daten (Handles, Werte, Befehle) auszutauschen. Dieser Vertrag ist in der DLL definiert GraphicStream.Immersive.Hub.Abstractions.

Konkret hast du zwei Wege:

  • Implementierung der Schnittstelle IConnector Für volle Kontrolle (du programmierst den gesamten Lebenszyklus und die Interaktionen).
  • Do Erbe die Klasse deines benutzerdefinierten Steckers von ConnectorBase Um Zeit zu sparen (Lebenszyklus, Fehlerbehandlung, Planung und Veröffentlichung werden bereits unterstützt), programmiert man nur das Geschäft).

Der Rest dieses Kapitels präsentiert Jedes Mitglied des Vertrags (IConnector) und erklärt, wie ConnectorBase bietet den Rahmen um die Umsetzung zu beschleunigen.

Schnittstelle IConnector — Mitglieder und Rolle

Schnittstellendeklaration:

/// <summary>
/// Defines the contract for a Hub connector.
/// </summary>
public interface IConnector
{
    /// <summary>
    /// Gets or sets the Hub context used to interact with the host.
    /// </summary>
    IHubContext? Context { get; set; }

    /// <summary>
    /// Gets or sets the connector display name.
    /// </summary>
    string Name { get; set; }

    /// <summary>
    /// Gets or sets the connector's description.
    /// </summary>
    string? Description { get; set; }

    /// <summary>
    /// Gets or sets the connector's pattern.
    /// </summary>
    string? Pattern { get; set; }

    /// <summary>
    /// Gets or sets the connector's connection string.
    /// </summary>
    string? ConnectionString { get; set; }

    /// <summary>
    /// Gets the <see cref="SessionStatus"/> of the current <see cref="OpcHubConnectorSession"/>
    /// </summary>
    Status Status { get; set; }

    /// <summary>
    /// Gets the connector definition associated to the current <see cref="IConnector"/> instance.
    /// </summary>
    ConnectorDefinition? Definition { get;}

    /// <summary>
    /// Gets the list of associated <see cref="ActivitySign"/>.
    /// </summary>
    System.Collections.Generic.IEnumerable<ActivitySign>? ActivitySigns { get; }

    /// <summary>
    /// Initialize the current <see cref="ConnectorBase"/> with the base useful parameters.
    /// </summary>
    Task Initialize();

    /// <summary>
    /// Connects the connector to its data source.
    /// Returns true if the connection succeeds.
    /// </summary>
    Task<bool> Connect();

    /// <summary>
    /// Disconnects and cleans up the connector.
    /// </summary>
    Task<bool> Disconnect();

    /// <summary>
    /// Return true if the current <see cref="IConnector"/> can handle the specified handle.
    /// </summary>
    /// <param name="handle"></param>
    /// <returns></returns>
    virtual bool CanHandle(IHandle handle) { return true; }

    /// <summary>
    /// Registers handles (paths) that the Hub wants to monitor.
    /// Returns true if at least one handle is supported.
    /// </summary>
    System.Collections.Generic.List<Handles.HandleRegistrationResponse> HandleHandles(IEnumerable<IHandle> handles);

    /// <summary>
    /// Requests the connector to write a value to the specified handle (output direction).
    /// </summary>
    /// <param name="handle">The handle representing the target data path.</param>
    /// <param name="value">The value to write to the target.</param>
    /// <returns>
    /// A task that completes with <c>true</c> if the write was successful, or <c>false</c> otherwise.
    /// </returns>
    Task<WriteValueResult> WriteValue(string path, string? value);

}

Detaillierte Beschreibung jedes Feldes/Eigenschafts/Verfahrens

  • Kontext: IHubContext?

Ermöglicht es dir, mit dem Hub zu interagieren (sich anzumelden, eine Handle-Änderung zu benachrichtigen, Abonnements zu speichern, auf Speicher zuzugreifen usw.).

  • Name: String:
    • Der Anzeigename des Anschlusses (lesbar auf der UI/Ops-Seite).
    • Nützlich, um mehrere Instanzen desselben Steckertyps zu unterscheiden.
    • Der Name kann vom Stecker eingestellt oder aus der Hub-Konfiguration geladen werden.
  • Beschreibung: Schnur?
    • Kostenlose Beschreibung (Zweck, Umfang, Zielumgebung).
    • Optional.
    • Die Beschreibung kann vom Stecker eingestellt oder aus der Hub-Konfiguration geladen werden.
  • Muster: Schnur?
    • Connector-spezifische Muster-/Filterstrings (z. B. Pfadmaske, Knotenauswahl usw.). Der vom Verbinder berücksichtigte Pfad kann auf einer Übereinstimmung mit dem Muster basieren. Dieses Tutorial zeigt ein Beispiel.
    • Optional; Geschäftsinterpretation durch den Connector.
    • Das Muster kann vom Stecker eingestellt oder aus der Hub-Konfiguration geladen werden.
  • ConnectionString: String?
    • Quellverbindungsparameter (URL, Zugangsdaten, Datenbank, Broker usw.).
    • Die Semantik hängt vom vom Connector unterstützten Protokoll/Quelle ab.
    • Der Verbindungsstring kann vom Stecker eingestellt oder aus der Hub-Konfiguration geladen werden.
  • Status: Status
    • Aktueller Steckerstatus (ENUM). Typische Werte: Disconnected, Connecting, Connected, ConnectError, DisconnectedWithCommunicationError, WorkingError.
    • Zu aktualisieren während Übergängen (Von Connect(), während Netzwerkfehlern usw.).
    • Der Status wird verwendet, um Hub-Administratoren oder Überwachungstools auf die Viabilität der vom Connector repräsentierten Datenquelle zu informieren.
  • Definition: ConnectorDefinition?
    • Strukturierte Definition/Konfiguration, die mit dem Connector verbunden ist (Metadaten, deklarative Eigenschaften).
    • Bereitgestellt durch die Hub-Konfiguration für diese Instanz.
    • Konfiguration ist eine Möglichkeit, einen Stecker flexibel einzurichten.
  • ActivitySigns: IEnumerible<ActivitySign>?
    • Aktivitätssignale/-indikatoren, die an den Stecker angeschlossen sind (z. B. Abfrageaktivität, Durchsatz usw.).
    • Optional; verwendet von Immersive Observability.
    • Aktivitätszeichen werden verwendet, um Hub-Administratoren oder Überwachungstools auf die Viabilität der vom Connector repräsentierten Datenquelle aufmerksam zu machen.
  • Initialisieren() : Aufgabe
    • "Kalte" Initialisierung: Felder (ConnectionString, Pattern usw.) validieren, Clients erstellen (HTTP, MQTT usw.), Mapper und Caches vorbereiten.
    • Muss idempotent sein und klare Ausnahmen einsetzen, wenn die Konfiguration ungültig ist.
  • Verbinden() : Aufgabe<bool>
    • Effektive Verbindung zur Quelle (Session etablieren, Händeschüttel, offene Abonnements).
    • Rückkehren true Wenn die Verbindung erfolgreich ist. Aktualisierungen Status folglich.
  • Trennen() : Aufgabe<bool>
    • Sauberer Abschluss der Verbindung (Abmelden, Spülen, Ressourcen entsorgen).
    • Rückkehren true wenn die Trennung gut verläuft.
  • CanHandle (IHandle Handle): bool
    • Gibt an, ob der Stecker einen bestimmten Griff verarbeiten kann.
    • Wird in einer konkreten Implementierung überschrieben, wenn der Connector nur eine Teilmenge von Pfaden unterstützt.
  • HandleHandles (z.B. numerable<IHandle> handles): Liste<HandleRegistrationResponse>
    • Speichert eine Liste von Handles, die vom Hub (zu überwachende/veröffentlichte Pfade) auf dem Connector angefordert wurden, über den Connector validiert wurde CanHandle.
    • Gibt für jeden Handle eine Datensatzantwort zurück (HandleRegistrationResponse) die effektive Unterstützung und den letzten verwendeten Schlüssel/Pfad angibt.
  • WriteValue(Zeichenzeichenpfad, Zeichenkette? Wert) : Aufgabe<WriteValueResult>
    • Schreibe (ausgehend) einen Wert zur Quelle auf einer Pfad (Ziel-Griff).
    • Gibt eine zurück WriteValueResult Erfolg/Misserfolg und je nach Art weitere Details anzeigen.

Baureihe ConnectorBase — Grundlage der Umsetzung

Die Klasse ConnectorBase wird in der DLL bereitgestellt GraphicStream.Immersive.Hub. Sie setzt bereits den Großteil des Vertrags um IConnector und bietet einen Standardrahmen: Verwaltung der Status, Wege (HandledPaths), Statusmeldungen (StateMessages), Integration mit der IHubContext und die IConnectorsManager.

Durch Erben ConnectorBase, du musst nur einige virtuelle Methoden überschreiben, um deine Protokolllogik einzufügen:

  • OnConnect() und OnDisconnect() : Um die Verbindung zur Datenquelle herzustellen oder zu schließen.
  • OnCanHandle(IHandle handle) : Um die Kriterien für die Unterstützung eines Handles festzulegen (Standard, basierend auf Pattern).
  • GetHandle(string path, DateTime? date) : Um den Wert eines Zielhandles zurückzugeben.
  • HandleHandles(IEnumerable<IHandle>) : Um die Registrierung neuer Handles zu übernehmen, die vom Hub angefordert werden.
  • WriteValue(string path, string? value) —Um einen Wert in die Datenquelle zu schreiben.

Formatierung von Pfaden & Verwendung eines \Muster\ (RegExp)

Der Hub kann laufen Mehrere Steckverbinder parallel. Wenn eine Sitzung eine Handle, der Hub nennt die CanHandle jedes Steckverbinders und behält die Erster Wer antwortet true. Standardmäßig basiert diese Entscheidung auf zwei Elementen: Der Staat Steckverbinder (IConnector.Status == Connected) und die Spielfeld IConnector.Pattern das als ein bewertet wird. Regulärer Ausdruck auf der Path des Griffs.

Um Mehrdeutigkeiten zwischen Verbindern zu vermeiden, sind die von den Griffe muss es den Verbindern ermöglichen, über zu sagen CanHandle ob die vom Handle anvisierten Daten von ihnen unterstützt werden können oder nicht.

Zum Beispiel Griffe Zieldaten, die von einem OPC-UA-Server gehostet werden, verfügen über Pfade wie:

  • nsu=namespaceUri; i=Ganze Zahl<
  • nsu=namespaceUri; s=Saite
  • nsu=namespaceUri; g=GUID
  • nsu=namespaceUri; b=base64string

Zum Beispiel:

ns=4;i=2

Unser Connector für unser Condominium Trustee-Lernprojekt muss auf eine Weise erkennen, dass eindeutig Die Wege, die Er dazugehören. Beginnen wir also mit folgender Standardisierung:

BuildingSensor://{deviceIdentifier}/{VariableName}
  • BuildingSensor:// : Wege zu den Daten des Syndikats sollten mit diesem beginnen Schema.
  • deviceIdentifier : Numerische oder alphanumerische Kennung der simulierten Ausrüstung.
  • VariableName : Variablenname (optional) für multivariate Geräte (z. B. floor, door, state…).

Wie bekommt man den Weg anerkannt?

Wir könnten einfach reinkommen OnCanHandle von ConnectorBase Überprüfen Sie einfach das Schema über Uri.Scheme == "buildingsensor": Es funktioniert und ist ausreichend. Um das Beispiel weiterzuführen und unser Lernen fortzusetzen, fügen wir Präzision und Filterung hinzu. Also werden wir:

  • Definiere ein Muster (regexp) Das erfasst unser Pfadformat.
  • Anpassen OnCanHandle um auf diesem Muster und dem Zustand des Steckers aufzubauen.

Das empfohlene Muster

Dieses Muster akzeptiert das "BuildingSensor://"-Schema, eine Gerätekennung und einen optionalen Variablennamen:

^buildingsensor:\/\/(?<device>[A-Za-z0-9_-]+)(?:\/(?<prop>[A-Za-z0-9_.:-]+))?$
  • (?<device>...) Erfasst die Geräte-ID.
  • (?<prop>...)? erfasst die Variable, falls vorhanden (optional).
  • Der Schema-Fall wird über das IgnoreCase.

Schritt-für-Schritt-Erstellung des Connectors zu BuildingSensorSimulator

Dieser Leitfaden beschreibt jeden Schritt, um Ihren Stecker richtig zu entwickeln. Alle wichtigen Methoden werden als implementiert dargestellt.

Klicken Sie hier, um das endgültige ASP.Net und das fertige Projekt herunterzuladen.

Schritt 1: Erstellen Sie das Visual Studio-Projekt

  1. Open Visual Studio > Klassenbibliothek (.NET).
  2. Nennen Sie das Projekt: MyCompany.Hub.Connectors.ConnectorToBuildingSensorSimulator.
  3. Ziel: .NET 9.0 (ausgerichtet mit dem Hub).

Schritt 2: Fügen Sie die DLL über NuGet hinzu

Statt eines ProjectReference installieren Sie das Abstraktions-DLLs für ein NuGet-Paket :

PM> Install-Package GraphicStream.Immersive.Hub.Abstractions
# ou en CLI :
dotnet add package GraphicStream.Immersive.Hub.Abstractions

Dies führt zu den Hub-Verträgen (IConnector, ConnectorBase, IHandle, HandleSnapshot, WriteValueResult usw.).

Schritt 3: Erstellen Sie die Connector-Klasse

Erstellen Sie eine Klasse und benennen Sie sie BuildingSensorSimulatorConnector. Erben von ConnectorBase um vom Lebenszyklus und der Sanitäranlagen zu profitieren. Bau- und Hauptbereiche:

public sealed class BuildingSensorSimulatorConnector : ConnectorBase, IDisposable
{
    private readonly Uri _baseUri;
    private readonly TimeSpan _pollInterval;
    private readonly HttpClient _httpClient;
    private readonly JsonSerializerOptions _jsonOptions;
    private CancellationTokenSource? _internalCts;
    private Task? _runLoop;
    private bool _disposed;
    private readonly System.Threading.Lock _interestGate = new();
    private const string SingleValueProperty = "value";
    private readonly ConcurrentDictionary<string, VariableSnapshot> _pathToLastSnapshot = new();

    public BuildingSensorSimulatorConnector()
    {
        this._baseUri = new Uri("https://localhost:7003");
        this._pollInterval = TimeSpan.FromSeconds(2);
        this._httpClient = new HttpClient();
        this._jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
        this.Name = "BuildingSensorSimulatorConnector";
    }
}

Wir definieren in Hard den Namen, der dem Connector gegeben werden soll, die Grundadresse der Simulation des Servers des Syndicate of Condominium Trustee und eine Wertsuche für einen Thread, der alle zwei Sekunden noch geschrieben werden muss.

Schritt 4: Definiere das Pfadschema und die Klasse Path

Tracked handles verwenden das Schema BuildingSensor://{deviceIdentifier}/{VariableName}. Lass uns die Klasse schreiben Path die sauber extrahiert werden DeviceIdentifier und Eigentum (Variablenname) aus einem IHandle :

/// <summary>
/// <para>BuildingSensor://{deviceIdentifier}/{VariableName}</para>
/// </summary>
/// <param name="deviceId">Device identifier.</param>
/// <param name="property">Property name.</param>
public class Path(string deviceId, string property)
{

    /// <summary>
    /// Gets or sets the device identifier associated to the current <see cref="Path Instance" />.
    /// </summary>
    public string DeviceIdentifier { get; set; } = deviceId;

    /// <summary>
    /// Gets or sets the property name associated to the current <see cref="Path.Property" />.
    /// </summary>
    public string Property { get; set; } = property;

    /// <summary>
    /// Extract a formated path from an <see cref="IHandle" /> instance.
    /// </summary>
    /// <param name="h"><see cref="IHandle" /> instance used to gets a formated path.</param>
    /// <returns>A formated path from the specified <see cref="IHandle" /> instance.</returns>
    public static Path FromHandle(IHandle h)
    {
        System.Uri uri = new(h.Path);
        return new(uri.Host, uri.LocalPath.Trim('/'));
    }

Schritt 5: Erkennen Sie die richtigen Wege

Wir stellen dann sicher, dass der Connector nur das Schema verarbeitet buildingsensor Via OnCanHandle

Es gibt zwei Möglichkeiten:

Schritt 5.a: Schnelle und einfache Erkennung

A OnCanHandle Es könnte einfach sein, zu überprüfen, ob PATH befüllt ist, dann ob es eine brauchbare URL ist, und schließlich, dass es mit beginnt mit buildingsensor:// :

public override bool OnCanHandle(IHandle handle)
{
    if (string.IsNullOrWhiteSpace(handle.Path)) return false;
    if (!Uri.TryCreate(handle.Path, UriKind.Absolute, out var uri)) return false;
    return uri.Scheme.Equals("buildingsensor", StringComparison.OrdinalIgnoreCase);
}

Diese Methode ist schnell und einfach, erlaubt aber Pfade, die nicht unbedingt formatiert sind, um die Geräte-ID und Variable zu extrahieren, sodass der folgende Pfad gültig wäre:

buildingsensor://iam_#not(_a_valide_path)

Schritt 5.b: Genaue Erkennung

Hier konnten wir uns auf einen regulären Ausdruck verlassen, der in der Pattern der IConnector

Schritt 1) Definieren Sie die Pattern (z. B. im Builder oder über die Hub-Konfiguration):

/// <summary>
/// Initializes a new instance of the <see cref="BuildingSensorSimulatorConnector"/> class.
/// </summary>
public BuildingSensorSimulatorConnector()
{
    this._pathToHandle = [];
    this._baseUri = new Uri("https://localhost:7003");
    this._pollInterval = TimeSpan.FromSeconds(2);
    this._httpClient = new HttpClient();
    this._jsonOptions = new JsonSerializerOptions
    {
        PropertyNameCaseInsensitive = true
    };

    this.Name = "BuildingSensorSimulatorConnector";
    this.Pattern = @@"^buildingsensor://(?<device>[A-Za-z0-9_-]+)(?:/(?<prop>[A-Za-z0-9_.:-]+))?$";
}

Schritt 2) Erstelle oder ersetze die Methode OnCanHandle durch eine bundesstaatenbasierte und regexp-basierte Version:

/// <inheritdoc />
public override bool OnCanHandle(IHandle handle)
{
    // 1) Le connecteur doit être connecté pour accepter un handle.
    if (this.Status != Status.Connected)
        return false;

    // 2) Path requis.
    var path = handle?.Path;
    if (string.IsNullOrWhiteSpace(path))
        return false;

    // 3) S'assurer que Pattern est défini (fallback si oublié dans la config).
    if (string.IsNullOrWhiteSpace(this.Pattern))
        this.Pattern = @@"^buildingsensor:\/\/(?<device>[A-Za-z0-9_-]+)(?:\/(?<prop>[A-Za-z0-9_.:-]+))?$";

    // 4) Évaluer la regexp (Regexp du Hub) sur le Path.
    return System.Text.RegularExpressions.Regex.IsMatch(
        path,
        this.Pattern,
        System.Text.RegularExpressions.RegexOptions.IgnoreCase |
        System.Text.RegularExpressions.RegexOptions.CultureInvariant
    );
}

Schritt 6: Speichere die angeforderten Handles

HandleHandles Analysiere die Pfade, validiere das Diagramm, präge die Paare DeviceId/Property und gibt für jedes eine Datensatzantwort zurück:

/// <inheritdoc/>
public override List<HandleRegistrationResponse> HandleHandles(IEnumerable<IHandle> handles)
{
    var responses = new System.Collections.Generic.List<HandleRegistrationResponse>();

    // Make a temporary storage of asked id/names extracted from handles's path.
    var temporaryIdToNames = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);

    foreach (var h in handles)
    {
        var resp = HandleRegistrationResponse.FromHandle(h);
        try
        {
            // check path.
            if (!Uri.TryCreate(h.Path, UriKind.Absolute, out var uri) ||
                !uri.Scheme.Equals("buildingsensor", StringComparison.OrdinalIgnoreCase))
            {
                resp.SupportStatus = SupportStatus.Unknown;
                resp.Message = "Unsupported scheme (expected BuildingSensor://...)";
            }
            else
            {
                var p = MyCompany.Hub.Connectors.ConnectorToBuildingSensorSimulator.Path.FromHandle(h);
                var deviceIdentifier = p.DeviceIdentifier?.Trim();
                var propertyName = string.IsNullOrWhiteSpace(p.Property) ? SingleValueProperty : p.Property.Trim();

                if (string.IsNullOrEmpty(deviceIdentifier))
                {
                    resp.SupportStatus = SupportStatus.Unknown;
                    resp.Message = "Missing device identifier in path.";
                }
                else
                {
                    if (!temporaryIdToNames.TryGetValue(deviceIdentifier, out var set))
                        temporaryIdToNames[deviceIdentifier] = set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

                    set.Add(propertyName);
                    resp.SupportStatus = SupportStatus.Known;
                    resp.Message = $"Registered {deviceIdentifier}/{propertyName}.";
                }
            }
        }
        catch (Exception ex)
        {
            resp.SupportStatus = SupportStatus.Failed;
            resp.Message = $"Registration parsing error: {ex.Message}";
        }

        responses.Add(resp);
    }

    // merge what was registered here with previoulsy known
    lock (_interestGate)
    {
        foreach (var kv in temporaryIdToNames)
        {
            if (!_deviceIdentifierToVariableNames.TryGetValue(kv.Key, out var set))
            {
                set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                _deviceIdentifierToVariableNames[kv.Key] = set;
            }

            foreach (var prop in kv.Value)
                set.Add(prop); // no double occurences
        }
    }

    // Report registration
    Context?.AddStateMessage(LogMessage.Info($"Registered interests: {string.Join(", ", temporaryIdToNames.Select(kv => $"{kv.Key}/{string.Join("|", kv.Value)}"))}"));

    return responses;
}

Für jede IHandle Empfangen, wir stellen sicher, dass wir eine zurückschicken HandleRegistrationResponse mit etwas, das er dem Hub sagen möchte, der diese Funktion aufruft, falls die IHandle analysiert, erkannt und dass es machbar ist.'

Um die nachfolgende Verarbeitung zu erleichtern, schützen wir die Assoziationen Geräte-ID und Variablennamen.

Schritt 7: Umfragen starten/stoppen

OnConnect() Beginnt die Schleife; Stop() und Dispose() Richtig adoptieren:

public override Task<bool> OnConnect()
{
    RunAsync(CancellationToken.None);
    return Task<bool>.FromResult(true);
}

public Task RunAsync(CancellationToken cancellationToken)
{
    ObjectDisposedException.ThrowIf(this._disposed, this);
    if (this._runLoop != null) throw new InvalidOperationException("The connector is already running.");

    this._internalCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
    this._runLoop = Task.Run(() => RunLoopAsync(_internalCts.Token), CancellationToken.None);
    return this._runLoop;
}

public void Stop()
{
    try { _internalCts?.Cancel(); } catch { /* ignore */ }
}

public override void Dispose()
{
    if (this._disposed) return;
    this._disposed = true;
    Stop();
    try { _runLoop?.Wait(TimeSpan.FromSeconds(2)); } catch { /* ignore */ }
    this._internalCts?.Dispose();
    this._httpClient.Dispose();
    base.Dispose();
}

Schritt 8: Polling Loop & Sensor Reading

Die Schleife ruft periodisch /api/Devices dann verarbeitet er den Schnappschuss:

/// <summary>
/// Main polling loop: fetches sensors and emits changes/logs.
/// </summary>
private async Task RunLoopAsync(CancellationToken ct)
{
    // Gate partagé (singleton ou service)
    var gate = new DistinctLogGate(reminderInterval: TimeSpan.FromMinutes(1));
    this.AddStateMessage($"Connecting to simulator at {_baseUri}");

    while (!ct.IsCancellationRequested)
    {
        try
        {
            gate.TryLogConnectorDistinct(this,"GettingList", LogMessage.Info("Getting list of sensors and device."));

            var sensors = await GetSensorsAsync(ct).ConfigureAwait(false);

            gate.TryLogConnectorDistinct( this, "ListGetted", LogMessage.Info($"{sensors.Count} getted"));

            ProcessSnapshot(sensors);
        }
        catch (OperationCanceledException) when (ct.IsCancellationRequested)
        {
            // Graceful shutdown.
            break;
        }
        catch (Exception ex)
        {
            this.AddErrorStateMessage($"Polling failed: {ex.Message}", ex.Message);
        }

        try
        {
            await Task.Delay(_pollInterval, ct).ConfigureAwait(false);
        }
        catch (OperationCanceledException) when (ct.IsCancellationRequested)
        {
            break;
        }
    }

    this.AddStateMessage("Connector stopped.");
}


private async Task<List<DeviceDto>> GetSensorsAsync(CancellationToken ct)
{
    var uri = new Uri(_baseUri, "/api/Devices");
    using var resp = await _httpClient.GetAsync(uri, ct).ConfigureAwait(false);
    resp.EnsureSuccessStatusCode();
    await using var stream = await resp.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
    var data = await JsonSerializer.DeserializeAsync<List<DeviceDto>>(stream, _jsonOptions, ct).ConfigureAwait(false);
    return data ?? [];
}

Schritt 9: Verarbeiten Sie den Schnappschuss und veröffentlichen Sie nur die Änderungen

Diese Methode analysiert die wiederhergestellten Geräte und ihre Werte und vergleicht sie mit dem, was der Stecker bereits über seine Wörterbücher hat. Wenn ein Datensatz als geändert gilt, muss er an den Hub zurückgesendet werden, um zu den interessierten Sitzungen zu kommen.

private void ProcessSnapshot(List<DeviceDto> devices)
{
    Dictionary<string, HashSet<string>> interests;
    lock (_interestGate)
    {
        interests = _deviceIdentifierToVariableNames.ToDictionary(
            kv => kv.Key,
            kv => new HashSet<string>(kv.Value, StringComparer.OrdinalIgnoreCase),
            StringComparer.OrdinalIgnoreCase);
    }
    if (interests.Count == 0) return;

    foreach (var d in devices)
    {
        var deviceKey = d.Id.ToString(CultureInfo.InvariantCulture);
        if (!interests.TryGetValue(deviceKey, out var wantedProps)) continue;

        if (d.Variables is { Count: > 0 })
        {
            foreach (var v in d.Variables)
            {
                if (!wantedProps.Contains(v.Name)) continue;
                var path = $"BuildingSensor://{deviceKey}/{v.Name}";
                var value = v.Value ?? string.Empty;
                var ts = v.TimestampUtc;
                PublishIfChanged(path, value, ts, v.Unit, HandleStatus.Good);
            }
        }
        else
        {
            if (!wantedProps.Contains(SingleValueProperty)) continue;
            var path = $"BuildingSensor://{deviceKey}/{SingleValueProperty}";
            var value = d.Value?.ToString(CultureInfo.InvariantCulture) ?? "";
            var ts = d.TimestampUtc;
            var unit = d.Unit;
            PublishIfChanged(path, value, ts!.Value, unit, HandleStatus.Good);
        }
    }
}

private void PublishIfChanged(string path, string value, DateTime changeDate, string? unit, HandleStatus status)
{
    var snap = new VariableSnapshot(value, changeDate);
    if (this._pathToLastSnapshot.TryGetValue(path, out var prev))
    {
        if (prev.Value == snap.Value && prev.TimestampUtc == snap.TimestampUtc) return;
    }
    this._pathToLastSnapshot[path] = snap;

    HandleSnapshot snapshot = new(
        path,
        value,
        status,
        changeDate,
        DateTime.UtcNow,
        unit,
        null);
    Context?.NotifyHandleChanged(snapshot);

    (HandleChanges as SimpleSubject<HandleSnapshot>)!.OnNext(snapshot);
}

Die Methode PublishIfChanged den zuletzt bekannten Wert des Weges mit dem Wert vergleicht, der aus dem Aufruf an das Simulationsprojekt abgerufen wurde.

Wenn eine Veränderung festgestellt wird, gilt ein HandleSnapshot instanziiert wird und der Wert dem Hub mitgeteilt wird, um ihn darüber zu informieren, dass der aktuelle Stecker eine Änderung des Wertes oder Zustands erkannt hat.'

Der Hub wird intern den Unterschied bestimmen Sitzungen die Interesse an diesem Weg gezeigt haben und ihnen den Zustand/Wert dieses Handles weitergeben.'

Schritt 10: Schreibt → Mapping zu Szenen

Die Methode WriteValue erlaubt es dem Hub, den Connector zu kontaktieren, um ihn zu bitten, einen Wert für den angegebenen Pfad zu schreiben. Es liegt an dem Hub, seine Datenquelle zu kontaktieren und die Daten an den von der Nation anvisierten Standort zu schreiben Verlauf. :

/// <inheritdoc/>
public override async Task<WriteValueResult> WriteValue(string path, string? value)
{
    // default result.
    var result = new WriteValueResult { IsSuccessFull = false };

    try
    {
        // 1) Parse path BuildingSensor://{deviceId}/{property}
        if (!Uri.TryCreate(path, UriKind.Absolute, out var bsUri) ||
            !bsUri.Scheme.Equals("buildingsensor", StringComparison.OrdinalIgnoreCase))
        {
            result.ExceptionMessage = "Unsupported path scheme (expected BuildingSensor://).";
            return result;
        }

        var deviceIdentifier = bsUri.Host;                      // ex: "101"
        var propertyName = bsUri.LocalPath.Trim('/');       // ex: "value", "door", "state", ...

        if (string.IsNullOrWhiteSpace(deviceIdentifier))
        {
            result.ExceptionMessage = "Missing device identifier in path.";
            return result;
        }

        // 2) gets the device to get its Type
        //    GET /api/Devices/{id}
        var deviceUri = new Uri(_baseUri, $"/api/Devices/{deviceIdentifier}");
        using var devResp = await _httpClient.GetAsync(deviceUri).ConfigureAwait(false);
        if (!devResp.IsSuccessStatusCode)
        {
            string message = $"Device {deviceIdentifier} not found (HTTP {(int)devResp.StatusCode}).";
            this.AddErrorStateMessage(message, null);
            result.ExceptionMessage = message;
            return result;
        }

        var devStream = await devResp.Content.ReadAsStreamAsync().ConfigureAwait(false);
        var device = await JsonSerializer.DeserializeAsync<DeviceDto>(devStream, _jsonOptions).ConfigureAwait(false);
        if (device is null)
        {
            result.ExceptionMessage = "Failed to deserialize device payload.";
            this.AddErrorStateMessage(result.ExceptionMessage, null);
            return result;
        }

        // 3) Compute scene + action
        //    - Type "light"  => SceneName = "Corridor Light", action on/off
        //    - Type "smoke"  => SceneName = "Fire Alarm",     action on/off
        //    - etc can extend here others devices
        string? sceneName = null;
        string? action = null;

        // Helper: transform value into bool (on/off)
        static bool IsTruthy(string? s)
        {
            if (string.IsNullOrWhiteSpace(s)) return false;
            s = s.Trim();
            if (bool.TryParse(s, out var b)) return b;
            if (double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var d)) return d > 0;
            return s.Equals("on", StringComparison.OrdinalIgnoreCase) ||
                    s.Equals("open", StringComparison.OrdinalIgnoreCase) ||
                    s.Equals("start", StringComparison.OrdinalIgnoreCase);
        }

        var truthy = IsTruthy(value);

        switch ((device.Type ?? "").ToLowerInvariant())
        {
            case "light":
                // "Corridor Light"
                sceneName = "Corridor Light";
                action = truthy ? "on" : "off";
                break;

            case "smoke":
                // "Fire Alarm"
                sceneName = "Fire Alarm";
                action = truthy ? "on" : "off";
                break;

            default:
                // Non supporté par le SceneController actuel
                result.ExceptionMessage = $"No scene mapping for device type '{device.Type}' (path: {path}).";
                this.AddErrorStateMessage(result.ExceptionMessage, null);
                return result;
        }

        // 4) Appel du SceneController : POST /api/Scene  { SceneName, Action }
        var sceneUri = new Uri(_baseUri, "/api/Scene");
        var payload = new { SceneName = sceneName, Action = action };
        var json = JsonSerializer.Serialize(payload, _jsonOptions);
        using var content = new StringContent(json, Encoding.UTF8, "application/json");

        using var resp = await _httpClient.PostAsync(sceneUri, content).ConfigureAwait(false);
        if (!resp.IsSuccessStatusCode)
        {
            var body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
            result.ExceptionMessage = $"Scene POST failed HTTP {(int)resp.StatusCode}: {body}";
            this.AddErrorStateMessage(result.ExceptionMessage, null);
            return result;
        }

        // 5) Succès
        result.IsSuccessFull = true;
        result.ReturnData = [sceneName, action];
        return result;
    }
    catch (Exception ex)
    {
        result.ExceptionMessage = ex.Message;
        this.AddErrorStateMessage(result.ExceptionMessage, null);
        return result;
    }
}

Diese Methode basiert auf den Möglichkeiten, die der Simulations-REST-Server bietet, einen Befehl über eine POST-Anfrage zu senden, nachdem das Gerät bestimmt wurde, das auf dem in der Signatur angegebenen Pfad angegeben ist.

Es initialisiert und befüllt eine Instanz des Typs WriteValueResult um das Ergebnis dieses Schreibens an den Hub zurückzusenden.

Schritt 11: DTOs & Datenstruktur

DTOs spiegeln die Nutzlast des Simulators wider (Einzelwert & Multivariable):

private sealed class DeviceDto
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Type { get; set; } = string.Empty;

    public string? Value { get; set; }
    public string? Unit { get; set; }
    public DateTime? TimestampUtc { get; set; }

    public List<DeviceVariableDto>? Variables { get; set; }
}

public sealed class DeviceVariableDto
{
    public string Name { get; set; } = string.Empty;
    public string Path { get; set; } = string.Empty;
    public string Kind { get; set; } = string.Empty;
    public string? Unit { get; set; }
    public string Value { get; set; } = string.Empty;
    public DateTime TimestampUtc { get; set; }
}

Quellen herunterladen