Hub: crea il tuo connettore
- Per iniziare
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 :
- Prepara un esempio concreto collegandosi a un'API REST che simula sensori di edifici
- Crea un connettore che possa recuperare ed esporre questi dati all'Hub.
Prerequisiti
- Ho installato e lanciato un hub
- Conoscere il ruolo e le funzionalità dell'Hub.
- 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:
- 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
- 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
- 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.
Lo sviluppo di un connettore su misura è essenziale per garantire la compatibilità tra il protocollo IoT del cliente e la piattaforma 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:
- 📦 BuildingSensorSimulator — v1.0 • 1,8 MB • ZIP
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"
}
Il connettore Immersive da sviluppare dovrà essere Consulta /api/Dispositivi (sondaggi o webhook se opportuno), Mappa ogni pezzo di apparecchiatura e/o variabile a un elemento del gemello (area, modello, ecc.), poi Aggiornamento i valori mostrati e gli stati (ascensore, porta del garage, ecc.). Le scene esposte da /api/Scene permettere di testare la catena end-to-end.
Il progetto è fornito in Visual Studio : Avvialo e poi consuma gli endpoint sopra direttamente dal tuo connettore. Potrai pubblicare l'archivio per il download in questo tutorial.
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
IConnectorPer il pieno controllo (programmi per tutto il ciclo di vita e le interazioni). - Do Eredita la classe del tuo connettore personalizzato da
ConnectorBasePer 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.
In sintesi: installa la DLL GraphicStream.Immersive.Hub.Abstractions, e poi o implementi IConnector, oppure derivi da ConnectorBase per beneficiare di una base solida e standardizzata.
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.).
In Immersive, il IHubContext viene iniettato come un Servizio attraverso il contenitore ASP.NET Core Dependency Injection.
Questo significa che non è necessario istianziarlo da solo: il motore lo inietta automaticamente in qualsiasi servizio o attore registrato e gestito dalla pipeline ASP.NET Core.
In particolare, se dichiari una dipendenza di tipo IHubContext nel costruttore di un servizio o di un componente (ad esempio un BackgroundService, a Controller, a HostedService o qualsiasi altro tipo aggiunto al contenitore), l'istanza attiva dell'Hub ti verrà fornita automaticamente.
⚠️ Attenzione: aggiungi IHubContext nel costruttore di una classe arbitraria non gestita da ASP.NET Core non funzionerà — l'iniezione viene effettuata solo per i tipi registrati nel contenitore di processo.
- 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.
- Stato attuale del connettore (ENUM). Valori tipici:
- 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
trueSe la connessione è riuscita. AggiornamentiStatusdi conseguenza.
- Disconnettimento() : Compito<bool>
- Chiusura netta della connessione (disiscrizione, svuotamento, eliminazione delle risorse).
- Ritorni
trueSe 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.
- Salva un elenco dei handle richiesti dall'Hub (percorsi da monitorare/pubblicare) sul connettore validato tramite
- WriteValue(percorso stringa, stringa? valore): Task<WriteValueResult>
- Scrivere (in uscita) un valore alla sorgente su un Percorso (manico bersaglio).
- Ritorna un
WriteValueResultindicando successo/fallimento e, a seconda del tipo, dettagli aggiuntivi.
Per notificare all'Hub un cambiamento di valore, il connettore crea un'istanza di HandleSnapshot e chiamate Context.NotifyHandleChanged(...). Per gli abbonamenti, usa Context.RegisterForChange(session, handleRequests). Pensa anche ad arricchire StateMessages tramite Context.AddStateMessage(...) per una chiara osservabilità.
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()eOnDisconnect(): Stabilire o chiudere la connessione con la fonte dati.OnCanHandle(IHandle handle): Per impostare i criteri per supportare un handle (predefinito, basato suPattern).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.
In pratica: si deriva da ConnectorBase, configuri le tue proprietà
(Name, Description, Pattern, ConnectionString…),
e sovrascrivi solo i metodi virtuali che corrispondono al tuo protocollo.
Tutta la gestione del ciclo di vita (stato, errori, messaggi di stato) è già fornita dal database.
In pratica: definire ConnectionString e Modello, implementa la vera connessione alla sorgente presa di mira dal connettore in OnConnect(), filtra ciò che supporti in CanHandle(), puoi riposare sul posto per questo Pattern, salva i tuoi percorsi tramite HandleHandles(), pubblica le modifiche con Context.NotifyHandleChanged(...) e implementare le voci tramite WriteValue(...). Stato deve riflettere accuratamente lo stato del connettore.
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…).
Esempio di percorsi validi:
BuildingSensor://101/floor: Percorso verso la variabile di pavimento dell'ID ascensore 101.BuildingSensor://201/door: Percorso verso la porta variabile della porta del garage G1 con identificatore 201.BuildingSensor://3oppureBuildingSensor://3/Value: Percorso verso il valore in Lux della luminosità della lampada del corridoio.
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
OnCanHandleper 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.
- 🔌 Connettore (BuildingSensor) — v1.0 • 8 KB • ZIP
Passo 1: Crea il progetto Visual Studio
- Open Visual Studio > Biblioteca di Classe (.NET).
- Nomina il progetto:
MyCompany.Hub.Connectors.ConnectorToBuildingSensorSimulator. - 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
);
}
Il risultato: solo un percorso nel formato BuildingSensor://{device}/{prop?} sarà accettato e Solo quando il connettore è in stato Connected. Questo previene collisioni con altri connettori, sfruttando al contempo il comportamento standard dell'Hub (pattern + status).
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.
Si noti l'uso dei tronchi alla fine di HandleHandles :
// Report registration
Context?.AddStateMessage(LogMessage.Info($"Registered interests: {string.Join(", ", temporaryIdToNames.Select(kv => $"{kv.Key}/{string.Join("|", kv.Value)}"))}"));
Accediamo al contesto Hub per chiamare il AddStateMessage. In questo modo possiamo tracciare più facilmente le attività del Connettore creato e i problemi che potrebbe incontrare.
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 ?? [];
}
Si noti l'uso di log intelligenti:
gate.TryLogConnectorDistinct(this,"GettingList", LogMessage.Info("Getting list of sensors and device."));
Il gate è un oggetto di tipo DistinctLogGate Istanziato appena sopra (preferisci creare istanze globali nei tuoi progetti).
gate analizzerà i log emessi dal connettore attuale e, invece di spammare gli utenti con log ripetitivi, emetterà il log collegato solo dopo un certo periodo di tempo (qui 1 minuto) o se un dato dal LogMessage si è evoluto dall'ultimo tentativo fallito di log.
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
- 📦 BuildingSensorSimulator — v1.0 • 1,8 MB • ZIP
- 🔌 Connettore (BuildingSensor) — v1.0 • 8 KB • ZIP