Hub: crea il tuo connettore

Training

Nel contesto della supervisione e automazione degli edifici, la soluzione Hub Immersivo Svolge un ruolo centrale in:

  • Lei Centralizza tutti i dati provenienti da fonti eterogenee (IoT, PLC, database, API REST, ecc.).
  • Lei Consegna applicazioni client (Beholder 3D, dashboard, script di automazione) informazioni aggiornate in tempo reale.
  • Permette ai tuoi sistemi di Parla una lingua comune, anche se i protocolli sottostanti sono diversi.

L'Hub, attraverso i suoi connettori, dispone di un gran numero di protocolli: OPC UA, MQTT, Excel ecc.

Ma se:

  • Il tuo Sensore o il tuo servizio sul campo non è supportato nativamente dall'Hub?
  • Devi collegare un API proprietaria o una fonte dati personalizzata ?

Ed è qui che entra in gioco la creazione di un connettore personalizzato.

A Connettore dell'hub agisce come un ponte tra la tua fonte di dati e l'Hub.

  • È Ascolti o domande Fonte esterna
  • È trasmette valori al Hub
  • Permette Manici registrato da ricevere dati aggiornati.

In questo tutorial, vedremo :

  1. Prepara un esempio concreto collegandosi a un'API REST che simula sensori di edifici
  2. Crea un connettore che possa recuperare ed esporre questi dati all'Hub.

Prerequisiti

  1. Ho installato e lanciato un hub
  2. Conoscere il ruolo e le funzionalità dell'Hub.
  3. Sapere come sviluppare in .Net.

Perché creare un connettore Hub personalizzato?

Nella soluzione Immersivo, Hub è il Nucleo dell'architettura dei dati. Agisce come un Direttore d'orchestra Tra:

  • Fonti dati (Sensori IoT, PLC, database, API REST, ecc.)
  • Consumatori (Beholder 3D, dashboard, script di automazione...)

L'Hub supporta già Molti protocolli standard ad esempio:

  • UCI UA per controllori industriali
  • Modbus per sistemi tecnici
  • MQTT per flussi IoT
  • REST e SQL per servizi e database tradizionali

Il caso tipico: distribuzione su un cliente con una sorgente non supportata

Durante il dispiegamento Immersivo in un Cliente finale, non è raro trovarsi in uno di questi scenari:

  1. Il client espone i dati tramite un protocollo proprietario
    • Esempio: un produttore di ascensori con un Protocollo TCP fatto in casa
    • Oppure un vecchio sistema BMS che pubblica i suoi dati su un Formato non standard
  2. Il cliente ha un'API specifica
    • Esempio: a Servizio web REST interno esposizione a temperature, consumi o allarmi
    • Oppure un Facciata dei Dati Cloud che richiede un'autenticazione personalizzata
  3. L'accesso ai dati richiede l'adattamento aziendale
    • Filtraggio, aggregazione o conversione delle unità prima che siano utilizzabili
    • Logica aggiunta per riportare solo il Valori di supervisione

In questi casi, L'Hub non può, così com'è, interrogare direttamente la sorgente. Senza un connettore dedicato, i dati rimanere inaccessibili per Beholder o per i tuoi scenari di automazione.

Il ruolo del connettore personalizzato

Crea un Connettore Hub Personalizzato Ti permette di:

  • Estensione dell'Hub a nuovi protocolli senza toccare il nucleo del software
  • Personalizzare l'accesso a API specifiche (REST, SOAP, file, streaming WebSocket...)
  • Trasformazione o filtrazione dei dati prima che arrivi in Immersive

In pratica:

  • Il connettore funge da traduttore tra la fonte cliente e l'Hub
  • Ogni maniglia salvata è associato a un percorso che il connettore sa come interpretare
  • L'Hub rimane generico mentre il tuo connettore gestisce la logica specifica

Presentazione del progetto

In questo progetto di esempio, stiamo supportando un Società di gestione condominiale ("Il Cliente" per il futuro) desiderando utilizzare la piattaforma Immersive per creare una Gemello digitale del suo edificio.

Il Dati IoT Le informazioni necessarie per la rappresentazione in tempo reale sono disponibili sull'infrastruttura del cliente, esposte tramite un'API REST interna.

Tuttavia, questi dati utilizzano un Protocollo specifico che attualmente non è supportata dai connettori esistenti di Immersive. Questo significa che sarà necessario Sviluppare un connettore dedicato per garantire l'integrazione e la visualizzazione delle informazioni nel gemello digitale.

Questo passaggio è un aspetto chiave per garantire che Continuità tra l'infrastruttura IoT del cliente e le capacità di rappresentazione e gestione 3D offerte da Immersive.

Presentazione dettagliata del progetto (scaricabile)

Il progetto per simulare l'API REST di questo sindacato di co-proprietà fornito è un Progetto Visual Studio (C# / ASP.NET Core) che replica l'infrastruttura degli edifici ed espone i dati tramite un'API REST. Serve come base per lo sviluppo di un Connettore immersivo adattato a questo protocollo non supportato nativamente.

Clicca qui per scaricare il progetto di simulazione ASP.Net:

Apparecchiature e sensori simulati

  • Temperatura : "Temperatura della stanza 101" (unità: °C).
  • Umidità : "Umidità della stanza 101" (unità: %).
  • Luce : "Livello luce del corridoio" (unità: lux).
  • Presenza : "Presenza in ufficio" (booleano).
  • Energia : "Contatore Elettrico Principale" (unità: kWh, cumulativo).
  • Fumo : "Rilevatore di fumo" (booleano).
  • Ascensore : "Sollevamento A" (multivariabile: pavimento digitale, Stato Enum, porta enum).
  • Porta del garage : "Garage G1" (multivariabile: porta Enum, Ostacolo bool).

Endpoint REST esposti

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).

Modello dati (risposte JSON)

Equipaggiamento a valore singolo (ad es. "Livello Luce del Corridoio"):

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

Questo tipo di apparecchiatura espone un solo valore tramite il "valore".

Equipaggiamento multivariabile – "Ascensore A":

{
  "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"
    }
  ]
}

Equipaggiamento multivariabile – Porta del garage "Garage G1":

{
  "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"
    }
  ]
}

Questa attrezzatura espone diverse variabili tramite il "Variabile".

Esempi di chiamate API

Elenca tutta l'attrezzatura

GET /api/Devices

Recupera l'attrezzatura tramite id

GET /api/Devices/101

Innescate una scena

Scene supportate: Faro del corridoio / Luce del corridoio (acceso/spento), Allarme antincendio / Allarme antincendio (acceso/spento).

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

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

Risposta esempio:

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

Ora che sono state realizzate le presentazioni con la fonte di dati del cliente, possiamo iniziare a pensare all'implementazione del nostro Connettore.

DLL di astrazione: Contratto da rispettare (IConnector) e base di implementazione (ConnectorBase)

Per sviluppare un connettore che sia riconosciuto dall'Hub, deve essere necessario Rispetto di un contratto comune: è questo contratto che permette all'Hub di identificare il connettore, orchestrare il suo ciclo di vita e scambiare dati (handle, valori, comandi). Questo contratto è definito nella DLL GraphicStream.Immersive.Hub.Abstractions.

In termini concreti, hai due percorsi:

  • Implementa l'interfaccia IConnector Per il pieno controllo (programmi per tutto il ciclo di vita e le interazioni).
  • Do Eredita la classe del tuo connettore personalizzato da ConnectorBase Per risparmiare tempo (ciclo di vita, gestione degli errori, programmazione e pubblicazione già supportati, programmi solo il business).

Il resto di questo capitolo presenta Ogni membro del contratto (IConnector) e spiega come ConnectorBase fornisce il quadro per accelerare l'implementazione.

Interfaccia IConnector — Membri e ruolo

Dichiarazione dell'Interfaccia:

/// <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);

}

Descrizione dettagliata di ogni campo/proprietà/metodo

  • Contesto: IHubContesto?

Ti permette di interagire con l'Hub (log, notificare un cambio di indirizzo, salvare abbonamenti, accedere allo spazio di archiviazione, ecc.).

  • Nome: stringa
    • Il nome visualizzato del connettore (leggibile sul lato UI/operazioni).
    • Utile per distinguere tra più istanze dello stesso tipo di connettore.
    • Il nome può essere impostato dal connettore o caricato dalla configurazione Hub.
  • Descrizione: string?
    • Descrizione libera (scopo, scopo, ambiente bersaglio).
    • Opzionale.
    • La descrizione può essere impostata dal connettore o caricata dalla configurazione dell'hub.
  • Modello: corda?
    • Schema/stringa di filtro specifica per il connettore (ad esempio path mask, selezione dei nodi, ecc.). Il percorso preso in considerazione dal connettore può basarsi su una corrispondenza con il pattern. Questo tutorial mostra un esempio.
    • Opzionale; Interpretazione commerciale da parte del connettore.
    • Il pattern può essere impostato dal connettore o caricato dalla configurazione Hub.
  • ConnectionString: stringa?
    • Parametri di connessione sorgente (URL, credenziali, database, broker, ecc.).
    • La semantica dipende dal protocollo/sorgente supportato dal connettore.
    • La stringa di connessione può essere impostata dal connettore o caricata dalla configurazione Hub.
  • Stato: Stato
    • Stato attuale del connettore (ENUM). Valori tipici: Disconnected, Connecting, Connected, ConnectError, DisconnectedWithCommunicationError, WorkingError.
    • Da aggiornare durante le transizioni (Da Connect(), durante errori di rete, ecc.).
    • Lo stato viene utilizzato per avvisare gli amministratori dell'Hub o gli strumenti di monitoraggio della fattibilità della fonte dati rappresentata dal connettore.
  • Definizione: ConnettoreDefinizione?
    • Definizione/configurazione strutturata associata al connettore (metadati, proprietà dichiarative).
    • Fornito dalla configurazione dell'hub per questa istanza.
    • La configurazione è un modo per configurare un connettore in modo flessibile.
  • AttivitàSegnali: Innumerevoli<ActivitySign>?
    • Segnali/indicatori di attività collegati al connettore (ad esempio, attività di sondaggio, throughput, ecc.).
    • Opzionale; utilizzato da osservabilità immersiva.
    • I segni di attività vengono utilizzati per avvisare gli amministratori dell'Hub o gli strumenti di monitoraggio della fattibilità della sorgente dati rappresentata dal connettore.
  • Inizializza() : Compito
    • Inizializzazione "a freddo": valida i campi (ConnectionString, Pattern, ecc.), crea client (HTTP, MQTT, ecc.), prepara mapper e cache.
    • Deve essere idempotente e lanciare eccezioni chiare se la configurazione è invalida.
  • Connect() : Compito<bool>
    • Connessione efficace con la fonte (stabilire sessione, stringere la mano, aprire abbonamenti).
    • Ritorni true Se la connessione è riuscita. Aggiornamenti Status di conseguenza.
  • Disconnettimento() : Compito<bool>
    • Chiusura netta della connessione (disiscrizione, svuotamento, eliminazione delle risorse).
    • Ritorni true Se la disconnessione fosse andata bene.
  • CanHandle (IHandle handle): bool
    • Indica se il connettore può gestire una determinata maniglia.
    • Da sovrascrivere in un'implementazione concreta se il connettore supporta solo un sottoinsieme di percorsi.
  • HandleHandles(<IHandle>IEnumerable handles): List<HandleRegistrationResponse>
    • Salva un elenco dei handle richiesti dall'Hub (percorsi da monitorare/pubblicare) sul connettore validato tramite CanHandle.
    • Restituisce, per ogni handle, una risposta a record (HandleRegistrationResponse) indica il supporto effettivo e la chiave/percorso finale utilizzato.
  • WriteValue(percorso stringa, stringa? valore): Task<WriteValueResult>
    • Scrivere (in uscita) un valore alla sorgente su un Percorso (manico bersaglio).
    • Ritorna un WriteValueResult indicando successo/fallimento e, a seconda del tipo, dettagli aggiuntivi.

Classe ConnectorBase — Base di implementazione

La classe ConnectorBase è fornito nella DLL GraphicStream.Immersive.Hub. Sta già attuando la maggior parte del contratto IConnector e fornisce un quadro standard: Gestione della Status, percorsi (HandledPaths), messaggi di stato (StateMessages), Integrazione con il IHubContext e il IConnectorsManager.

Ereditando ConnectorBase, devi solo sovrascrivere alcuni metodi virtuali per collegare la logica del protocollo:

  • OnConnect() e OnDisconnect() : Stabilire o chiudere la connessione con la fonte dati.
  • OnCanHandle(IHandle handle) : Per impostare i criteri per supportare un handle (predefinito, basato su Pattern).
  • GetHandle(string path, DateTime? date) : Per restituire il valore di un handle mirato.
  • HandleHandles(IEnumerable<IHandle>) : Per gestire la registrazione dei nuovi nick richiesti dall'Hub.
  • WriteValue(string path, string? value) —Scrivere un valore nella fonte dati.

Formattazione dei percorsi e uso di un \Pattern\ (RegExp)

Il Hub può funzionare Connettori multipli in parallelo. Quando una sessione offre un Handle, il Hub chiama il CanHandle di ciascun connettore e mantiene il Primo chi risponde true. Per impostazione predefinita, questa decisione si basa su due elementi: Lo stato connettore (IConnector.Status == Connected) e il Campo IConnector.Pattern che viene valutato come un Espressione regolare sul Path del manico.

Per evitare ambiguità tra i connettori, i percorsi esposti dal Manici deve permettere ai connettori di poter comunicare tramite CanHandle se i dati presi a mira dal handle possano essere supportati da loro o meno.

Ad esempio, Manici i dati di targeting ospitati da un server OPC UA avranno percorsi come:

  • nsu=namespaceUri; i=intero<
  • nsu=namespaceUri; s=stringa
  • nsu=namespaceUri; g=guid
  • nsu=namespaceUri; b=base64string

Ad esempio:

ns=4;i=2

Il nostro connettore per il nostro progetto di apprendimento Condominium Trustee deve riconoscere in modo che inequivocabile i sentieri che Lui appartenere. Quindi iniziamo con la seguente standardizzazione:

BuildingSensor://{deviceIdentifier}/{VariableName}
  • BuildingSensor:// : I percorsi verso i dati del sindacato dovrebbero partire da questo Schema.
  • deviceIdentifier : Identificatore numerico o alfanumerico dell'apparecchiatura simulata.
  • VariableName : nome variabile (opzionale) per dispositivi multivariati (ad es. floor, door, state…).

Come far riconoscere il percorso?

Potremmo semplicemente entrare OnCanHandle di ConnectorBase Basta controllare lo schema tramite Uri.Scheme == "buildingsensor": Funziona ed è sufficiente. Per portare l'esempio oltre e continuare a imparare, aggiungiamo precisione/filtraggio. Quindi voglio di:

  • Definiamo un Modello (regexp) che cattura il nostro formato di percorso.
  • Adattarsi OnCanHandle per costruire su questo pattern e sullo stato del connettore.

Il modello consigliato

Questo modello accetta lo schema "BuildingSensor://", un identificatore dell'equipaggiamento e un nome di variabile opzionale:

^buildingsensor:\/\/(?<device>[A-Za-z0-9_-]+)(?:\/(?<prop>[A-Za-z0-9_.:-]+))?$
  • (?<device>...) Rileva l'ID del dispositivo.
  • (?<prop>...)? cattura la variabile se presente (Opzionale).
  • Il caso dello schema viene gestito tramite il IgnoreCase.

Creazione passo dopo passo del connettore a BuildingSensorSimulator

Questa guida dettaglia ogni fase per sviluppare correttamente il connettore. Tutti i metodi chiave sono mostrati come implementati.

Clicca qui per scaricare il progetto finale ASP.Net e pronto per compilare.

Passo 1: Crea il progetto Visual Studio

  1. Open Visual Studio > Biblioteca di Classe (.NET).
  2. Nomina il progetto: MyCompany.Hub.Connectors.ConnectorToBuildingSensorSimulator.
  3. Obiettivo: .NET 9.0 (allineata con l'Hub).

Passo 2: Aggiungi la DLL tramite NuGet

Invece di un ProjectReference, installa il DLL di astrazione per un Pacchetto NuGet :

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

Questo porta ai contratti Hub (IConnector, ConnectorBase, IHandle, HandleSnapshot, WriteValueResult, ecc.).

Passo 3: Crea la classe connettore

Crea una classe e chiamala BuildingSensorSimulatorConnector. Ereditare da ConnectorBase per beneficiare del ciclo di vita e dell'impianto idraulico. Costruttore e campi principali:

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";
    }
}

Definiamo, in hard , il nome da assegnare al connettore, l'indirizzo base della simulazione del server del Syndicate of Condominium Trustee e una ricerca di valore per un thread che rimane da scrivere ogni 2 secondi.

Passo 4: Definisci lo schema del percorso e la classe Path

Le maniglie tracciate utilizzano lo schema BuildingSensor://{deviceIdentifier}/{VariableName}. Scriviamo la lezione Path che estrarrà pulito DeviceIdentifier e Proprietà (nome variabile) da un 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('/'));
    }

Passo 5: Riconosci i percorsi giusti

Poi ci assicuriamo che il connettore gestisca solo lo schema buildingsensor tramite OnCanHandle

Ci sono due possibilità:

Passo 5.a: Riconoscimento rapido e semplice

A OnCanHandle semplice potrebbe essere controllare che PATH sia popolato, poi che sia un URL valido e infine che inizi con 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);
}

Questo metodo è rapido e semplice, ma permette ai percorsi non necessariamente formattati di estrarre l'ID e la variabile del dispositivo, quindi il seguente percorso sarebbe valido:

buildingsensor://iam_#not(_a_valide_path)

Passo 5.b: Riconoscimento accurato

Qui potevamo fare affidamento su un'espressione regolare, memorizzata nel Pattern del IConnector

Passo 1) Definire il Pattern (ad esempio nel costruttore o tramite la configurazione Hub):

/// <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_.:-]+))?$";
}

Passo 2) Crea o sostituisci il metodo OnCanHandle Da una versione basata su stato e regexp:

/// <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
    );
}

Passo 6: Salva i handle richiesti

HandleHandles analizza i percorsi, valida il diagramma, memorizza le coppie DeviceId/Property e restituisce una risposta record per ciascuno:

/// <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;
}

Per ciascuno IHandle ricevuto, assicurarci di inviare indietro un HandleRegistrationResponse con qualcosa da dire all'Hub che chiama questa funzione se il IHandle è stata analizzata, riconosciuta e che è valida.'

Per facilitare il successivo trattamento, tuteliamo le associazioni ID dispositivo e Nomi delle variabili.

Passo 7: Avvio/Stop del sondaggio

OnConnect() Inizia il ciclo; Stop() e Dispose() Adottare correttamente:

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();
}

Passo 8: Loop di sondaggio e lettura dei sensori

Il loop chiama periodicamente /api/Devices poi processa l'istantaneo:

/// <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 ?? [];
}

Passo 9: Elabora lo snapshot e pubblica solo le modifiche

Questo metodo analizzerà i dispositivi recuperati e i loro valori e li confronterà con ciò che il connettore ha già tramite i suoi dizionari. Se un dato viene considerato modificato, deve essere inviato all'Hub per partecipare alle sessioni interessate.

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);
}

Il metodo PublishIfChanged confronterà l'ultimo valore noto del percorso con il valore recuperato dalla chiamata al progetto di simulazione.

Se viene rilevata una modifica, un HandleSnapshot viene istanziato e il valore viene notificato all'Hub per notificarlo che il connettore corrente ha rilevato un cambiamento di valore o stato.'

L'Hub internamente determinerà le diverse differenze Sessioni che hanno mostrato interesse per questo percorso e trasmettono loro lo stato/valore di questo manico.'

Passo 10: Scrive → mappando le scene

Il metodo WriteValue permette all'Hub di contattare il connettore per chiedergli di scrivere un valore per il percorso specificato. Spetta all'Hub contattare la propria fonte di dati e scrivere i dati nella posizione presa di mira dal Percorso. :

/// <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;
    }
}

Questo metodo si basa sulle possibilità offerte dal server REST di simulazione per inviare un ordine tramite una richiesta POST dopo aver determinato il dispositivo bersaglio dal percorso fornito nella firma del metodo.

Inizializza e popola un'istanza di tipo WriteValueResult per inviare il risultato di questo scrivere di nuovo all'Hub.

Passo 11: DTO e Struttura Dati

I DTO riflettono il carico utile del simulatore (a valore singolo e multivariabile):

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; }
}

Download delle fonti