Sommaire
Etant donné que chaque package peut (et devrait) déclarer la liste des MessageCallbacks détaillée (description, liste des paramètres, type de réponse, …) qu’il expose ainsi que la liste des types personnalisées qu’il utilise dans la signature de ses MessageCallbacks ou des StateObjects qu’il publie, il est donc possible de générer du code de manière automatique.
C’est grâce à cette description, que l’on nomme le “PackageDescriptor” que fonctionne le “MessageCallback Explorer” de la Console Constellation par exemple.
L’interface est capable de lister chaque MessageCallbacks de chaque packages de votre Constellation avec un formulaire pour la saisie des paramètres (simples ou complexes) afin de tester simplement vos MC.
En cliquant sur le bouton , le “MessageCallback Explorer” de la Console Constellation vous propose des “code snippets” pour différents langages (C#, Python, Arduino, JS, etc.).
Pourquoi générer du code ?
Comme vous le savez, en C# pour envoyer un message et donc invoquer un MC d’un autre package, vous devez créer un scope et invoquer le MC en utilisant un proxy dynamique.
Par exemple pour invoquer le MC “AreaArm” du package Paradox, on pourrait écrire :
1 |
PackageHost.CreateMessageProxy("Paradox").AreaArm(new { Area = Paradox.Core.Area, Mode = Paradox.Core.ArmingMode, PinCode = System.String }); |
Il est important de noter ici que la méthode “CreateMessageProxy” retourne un “proxy dynamique”, c’est à dire que la méthode invoquée, ici “AreaArm” sera la clé du message.
Autrement dit, comme tout est dynamique, il y a aucune aide ou auto-complétion. On pourrait très bien écrire ceci :
1 |
PackageHost.CreateMessageProxy("Paradox").CeciEstUnExemple(); |
Votre package enverra un message “CeciEstUnExemple” au(x) package(s) Paradox de votre Constellation. Vous devez donc être vigilent sur le nom des MC invoqués car une erreur de frappe passera inaperçu !
Pour plus d’information sur l’envoi de messages & invocation de MessageCallbacks en C#, veuillez lire ceci.
Le “problème” est le même avec la consommation des StateObjects (lire ceci). Par exemple pour injecter dans votre code, le StateObject “/intelcpu/0/load/0” produit par le package “HWMonitor” sur la sentinelle “MON-PC”, on peut écrire :
1 2 |
[StateObjectLink("MON-PC", "HWMonitor", "/intelcpu/0/load/0")] private StateObjectNotifier CPU { get; set; } |
Je peux donc ensuite exploiter la valeur de mon StateObject :
1 |
dynamic value = this.CPU.DynamicValue; |
La propriété “Value” du StateObjectNotifier me donne la valeur du StateObject et la propriété “Value” de ce StateObject me donne la valeur du StateObject. Vous pouvez utiliser la propriété “DynamicValue” directement sur le StateObjectNotifier pour récupérer la valeur du SO sous forme d’un ”dynamic”.
Comme chaque StateObject est différent, je ne sais pas ne que je trouverai dedans (une valeur simple, un objet complexe, …). D’où l’intérêt d’utiliser le StateObject Explorer de la Console Constellation pour explorer les SO de votre Constellation.
Par exemple, le StateObject “/intelcpu/0/load/0” produit par le package “HWMonitor” est un objet complexe contenant 4 propriétés :
Alors que le StateObject de type “CarbonDioxideMeasurement” produit par le package “NetAtmo” est un entier :
C’est pour cela que dans votre code, la valeur d’un StateObject est “dynamique” : tout dépend du StateObject que vous consommez !
Que ce soit pour l’invocation de MessageCallback ou la consommation de StateObject, la forme “dynamique” permet de s’adapter à toutes les situations. En revanche vous perdez l’auto complétion ce qui peut vous ralentir mais aussi être une source d’erreur. D’où l’intérêt de générer du code dans votre package!
Générer du code depuis Visual Studio.
Le générateur de code inclut dans le SDK Constellation pour Visual Studio ne fonctionne que pour un projet C#.
Commencez tout d’abord par cliquer le bouton “Generate Code” dans la barre de menu :
Le code sera généré pour le projet marqué comme projet de démarrage dans le cas où votre solution contient plusieurs projets.
Autrement, en cliquant-droit sur le projet de votre choix, sélectionnez “Generate Code” dans le sous-menu “Constellation” :
Vous serez amené à sélectionner dans la liste déroulante la Constellation à cibler. Pour configurer des connexions vers vos Constellations, lisez ceci.
Une fois le serveur Constellation sélectionné, cliquez sur “Connect and Discover”. Vous obtiendrez la liste de toutes vos sentinelles et packages de votre Constellation :
Sélectionnez tout simplement la liste des packages que vous souhaitez inclure dans votre code. Par exemple, ici nous allons générer du code pour les packages “DoorBell”, “LightSensor”, “IRRemote”,”Paradox”, “Pionner” et “Vera”.
Après avoir cliqué sur le bouton “Generate”, un message de confirmation vous indiquera le bon déroulé de l’opération :
Le SDK génère le code dans le fichier “MyConstellation.generated.cs” :
Attention, vous ne devez pas modifier ce code directement car ce fichier est écrasé à chaque fois que vous relancer une génération.
Utiliser le code généré
Organisation du code généré
Le code généré dans le fichier “MyConstellation.generated.cs” s’organise dans plusieurs espaces de nom (namespaces) :
-
Dans le namespace de votre assembly vous trouverez :
-
La classe statique “MyConstellation” représentant votre Constellation
-
Les classes utilitaires RealNameAttribute et RealNameExtension indispensable au fonctionnement du code généré
-
-
Des namespaces par package
-
VotreNamespace.NomDuPackage.StateObjects : code généré pour les StateObjects (si des StateObjects sont déclarés pour le package)
-
VotreNamespace.NomDuPackage.MessageCallbacks : code généré pour les MessageCallbacks (si des MessageCallbacks sont déclarés pour le package)
-
Des énumérations pour les sentinelles, packages et instances de votre Constellation
La classe statique “MyConstellation” contient trois énumérations :
-
Sentinels : contenant la liste des sentinelles de votre Constellation
-
Packages : contenant le liste des packages de votre Constellation
-
PackageInstances : contenant la liste des instances des packages de votre Constellation
Comme les noms de sentinelles ou packages peuvent contenir des caractères interdits en C# (comme par exemple les tirets), les valeurs des énumérations sont “nettoyées” et la valeur réelle se trouve dans l’attribut “RealName” que vous pouvez récupérer avec la méthode d’extension “GetRealName()”.
Dans notre exemple l’énumération “Sentinels” générée est la suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
/// <summary> /// Specifies the sentinels in your Constellation /// </summary> public enum Sentinels { /// <summary> /// Sentinel 'CEREBRUM' /// </summary> [RealName("CEREBRUM")] CEREBRUM, /// <summary> /// Sentinel 'ESP8266' /// </summary> [RealName("ESP8266")] ESP8266, /// <summary> /// Sentinel 'ESP-DoorBell' /// </summary> [RealName("ESP-DoorBell")] ESP_DoorBell, /// <summary> /// Sentinel 'ESP-LightSensorSalon' /// </summary> [RealName("ESP-LightSensorSalon")] ESP_LightSensorSalon, /// <summary> /// Sentinel 'esp-senergy' /// </summary> [RealName("esp-senergy")] esp_senergy, /// <summary> /// Sentinel 'SKYNET-SERVER' /// </summary> [RealName("SKYNET-SERVER")] SKYNET_SERVER, } |
De ce fait on peut manipuler les sentinelles et récupérer le nom réel avec la méthode “GetRealName()” :
1 2 |
MyConstellation.Sentinels sentinel = MyConstellation.Sentinels.ESP_DoorBell; string realName = sentinel.GetRealName(); |
De plus, dans cette classe vous trouverez des méthodes d’extension pour créer un “MessageScope” vers une de vos sentinelles, packages ou instances de package.
Par exemple pour créer un scope vers les packages “Hue” :
1 |
MessageScope scope = MyConstellation.Packages.Hue.CreateScope(); |
Ce qui est équivalent à :
1 |
MessageScope scope = MessageScope.Create("Hue"); |
Sauf qu’avec le code généré plus besoin de chercher le nom exact ni même de risquer de faire une erreur de frappe, car tout est énumération !
On peut également cibler une instance d’un package en particulier. Par exemple pour cibler précisément le package “Hue” déployé sur la sentinelle “SKYNET-SERVER” :
1 |
MessageScope scope = MyConstellation.PackageInstances.SKYNET_SERVER_Hue.CreateScope(); |
Code généré pour les StateObjects
Prenons un exemple simple : dans le code généré ci-dessus j’ai sélectionné le package “Paradox”, un package permettant de connecter les système d’alarme Paradox dans Constellation.
Ce package publie plusieurs StateObjects :
-
Des StateObjects de type “AreaInfo” par secteur qui représente l’état d’un secteur (système armé ou non par exemple)
-
Des StateObjects de type “ZoneInfo” par zone qui représente l’état d’une zone (zone ouverte ou non par exemple)
-
Des StateObjects de type “UserInfo” par utilisateur qui représente l’état d’un utilisateur (nom de l’utilisateur, dernière activité, etc..)
Prenons par exemple le StateObject “ZoneInfo1” de ce package :
On y trouvera plusieurs informations sur l’état de cette zone.
Le code généré pour les StateObjects de ce package sera donc rangé dans le namespace : VotreNamespace.Paradox.StateObjects et contiendra :
-
Une énumération “ParadoxStateObjectNames” référençant le nom des StateObjects actuellement connus sur le serveur
-
Une classe “ParadoxStateObjectLinkAttribute” (spécialisation de la classe StateObjectLinkAttribute)
-
Des classes pour chaque types personnalisés du package, ici le générateur aura généré les classes “AreaInfo”, “ZoneInfo” et “UserInfo”
-
Une classe “ParadoxExtensions” contenant des méthodes d’extension pour convertir des StateObjects en type personnalisé
Voyons par exemple comment inclure notre StateObject de la zone “1” dans votre code C# avec le code généré.
Tout d’abord, il faut inclure le namespace :
1 |
using Paradox.StateObjects; |
Ensuite ajoutons un “StateObjectLink” de type “ParadoxStateObjectLink” où nous préciserons le nom du StateObject avec l’énumération :
1 2 |
[ParadoxStateObjectLink(ParadoxStateObjectNames.ZoneInfo1)] public StateObjectNotifier Zone1 { get; set; } |
Sans le code généré nous aurions écrit :
1 2 |
[StateObjectLink(Package="Paradox", Name="ZoneInfo1")] public StateObjectNotifier Zone1 { get; set; } |
Je peux ensuite utiliser la méthode d’extension générée “AsZoneInfo()” pour convertir la valeur du StateObject en “ZoneInfo” (“ZoneInfo” étant un type personnalisé décrit par le package Paradox) :
Le générateur a “reproduit” ce type dans votre code généré avec les commentaires tel que spécifiés dans le PackageDescriptor du package Paradox.
Sans le code généré, vous devez travailler avec un objet dynamique, donc sans auto-complétion :
Bien entendu tous les Packages (virtuels ou non) peuvent déclarer des Packages Descriptor.
Prenez l’exemple d’un capteur d’électricité basé sur un ESP8266. En utilisant la librairie Constellation pour Arduino, le code C++ de ce package virtuel commence par déclarer le type “SEnergy.Electricity” :
1 2 |
constellation.addStateObjectType("SEnergy.Electricity", TypeDescriptor().setDescription("S-Energy Electricity data").addProperty("Counter", "System.Int64", "Number of revolution").addProperty("Timestamp", "System.Int64", "Internal timestamp of the last revolution").addProperty("RevolutionTime", "System.Double", "The time (in ms) of the last revolution").addProperty("WattPerHour", "System.Int32", "Energy consumed").addProperty("Cumul", "System.Int64", "Total of KWh consumed")); constellation.declarePackageDescriptor(); |
Puis à chaque fois que le capteur détecte une consommation électrique il publie un StateObject de la façon suivante :
1 2 3 4 5 6 7 8 |
StaticJsonBuffer<JSON_OBJECT_SIZE(5)> jsonBuffer; JsonObject& myStateObject = jsonBuffer.createObject(); myStateObject["Counter"] = counter; myStateObject["Timestamp"] = ts; myStateObject["RevolutionTime"] = timePerRevolution; myStateObject["WattPerHour"] = wattPerHour; myStateObject["Cumul"] = cumul; constellation.pushStateObject("Electricity", myStateObject, "SEnergy.Electricity", 600); |
On retrouve bien ce StateObject sur la Console Constellation :
Après avoir sélectionné le package “SElectricity” dans le générateur de code, je peux très facilement exploiter ce StateObject avec auto-complétion, description, etc.. :
Par exemple pour suivre en temps réel la consommation électrique dans mon package C# avec le code généré à partir du capteur ESP8266 écrit en C++/Arduino :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
namespace ConstellationPackageConsole2 { using Constellation.Package; using ConstellationPackageConsole2.SElectricity.StateObjects; public class Program : PackageBase { [SElectricityStateObjectLink(SElectricityStateObjectNames.Electricity)] public StateObjectNotifier Electricity { get; set; } static void Main(string[] args) { PackageHost.Start<Program>(args); } public override void OnStart() { this.Electricity.ValueChanged += (s, e) => { PackageHost.WriteInfo($"Current Energy Consumption : {e.NewState.AsSElectricitySEnergy_Electricity().WattPerHour}W @ {e.NewState.LastUpdate}"); }; } } } |
Code généré pour les MessageCallbacks
Le principe est le même avec les MessagesCallbacks, le générateur de code va créer le code dans le namespace “VotreNamespace.NomDuPackage.MessagesCallbacks” :
-
Des classes pour chaque types personnalisés utilisés dans les MC du package
-
Une classe “(NomDuPackage)Scope” permettant de référencer les MC sous forme de méthodes .NET
-
Une classe “(NomDuPackage)Extensions” : classe d’extension pour créer un scope du package à partir d’un MessageScope ou des énumérations Sentinels, Packages, PackagesInstances générées par le générateur
MessageCallbacks avec ou sans paramètre
Prenons par exemple le package virtuel “IRremote”, un récepteur/émetteur d’infrarouge développé en Arduino/C++ sur un ESP8266. Ce package virtuel expose deux MessageCallbacks : “Restart” pour rebooter l’ESP et “SendCode” pour envoyer un signal IR.
En utilisant la librairie Constellation pour Arduino de ces deux MC se résume par ces quelques lignes de C++ :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// SendCode MessageCallback constellation.registerMessageCallback("SendCode", MessageCallbackDescriptor().setDescription("Send the IR code").addParameter("encoding", "System.String").addParameter("code", "System.Int64"), [](JsonObject& json) { const char * encoder = json["Data"][0].asString(); unsigned long code = strtoul(json["Data"][1].asString(), NULL, 0); // send the "code" here ! }); // Restart MessageCallback constellation.registerMessageCallback("Restart", MessageCallbackDescriptor().setDescription("Restart the ESP"), [](JsonObject& json) { ESP.restart(); }); |
Une fois l’ESP démarré, nos deux MessageCallbacks sont correctement référencés sur Console Constellation avec les listes des paramètres, types et description :
Dans Visual Studio, générons maintenant le code pour notre package “IRRemote” :
Ajoutons ensuite le namespace correspondant aux MessageCallbacks de notre package, ici “IRRemote” :
1 |
using IRremote.MessageCallbacks; |
Vous pouvez ensuite utiliser l’énumération “Packages” (ou “PackagesIntances”) et accéder à la méthode d’extension “CreateIRRemoteScope” pour créer un scope spécifiquement pour notre package :
Vous avez également une méthode d’extension nommée “ToXXXXSCope()” sur un MessageScope. En clair vous avez plusieurs moyen de créer un scope spécifiquement pour votre package “IRremote” avec le code généré :
1 2 3 4 5 6 7 |
IRremoteScope scope = MyConstellation.Packages.IRremote.CreateIRremoteScope(); IRremoteScope scope = new IRremoteScope(MessageScope.Create("IRRemote")); IRremoteScope scope = MessageScope.Create("IRRemote").ToIRremoteScope(); IRremoteScope scope = MessageScope.Create(MyConstellation.Packages.IRremote.GetRealName()).ToIRremoteScope(); |
Ensuite sur la classe Scope généré pour votre package, ici “IRRemoteScope”, vous retrouverez vos MessageCallbacks sous forme de méthode .NET avec les paramètres et descriptions !
De ce fait vous disposez de l’auto complétion sans risque de faire des erreurs de frappe :
Ainsi pour envoyer un code IR sur l’Arduino/ESP depuis mon code C#, on pourrait écrire :
1 2 |
// Send Power ON/OFF to Samsung TV MyConstellation.Packages.IRremote.CreateIRremoteScope().SendCode("Samsung", 0xE0E040BF); |
MessageCallbacks avec des paramètres complexes
Ici le package “IRRemote” expose un MC sans paramètre et un autre avec deux paramètres simples (string et long).
Mais le générateur est également capable de gérer les types complexes. Par exemple prenez le package “Xbmc” permettant de piloter des média-centers Xbmc/Kodi.
Le package expose différents MessageCallbacks pour lancer un média, mettre pause, piloter le volume et également pour afficher un message à l’écran via le MessageCallback nommé “ShowNotification”.
Ce MessageCallback prend deux paramètres : le nom de l’hôte Xbmc (un string) et la notification à afficher. Cette notification est un objet de type “Xbmc.NotificationRequest” :
Sur la Console Constellation, vous pouvez cliquer sur les types personnalisés pour afficher les détails du type, ici un objet avec quatre propriétés :
Lorsque vous générez le code C# pour ce package vous retrouverez bien le MC “ShowNotification” avec le type personnalisé en paramètre :
Le générateur a en effet reproduit le type personnalisé dans votre code C# :
Ainsi pour afficher une notification sur un hôte Kodi, je pourrais écrire très simplement :
1 |
MyConstellation.Packages.Xbmc.CreateXbmcScope().ShowNotification("Kodi", new NotificationRequest() { Title = "Constellation", Message = "Hello World !" }); |
MessageCallbacks avec réponse : les sagas
Une invocation d’un MessageCallback peut donner lieu à une réponse, on appelle cela les “Sagas”. Avec l’API.NET, n’hésitez pas à relire les articles dédiés : Invoquer un MessageCallback avec réponse et Répondre à une saga.
Prenons un exemple, le package “Vera” (interface pour les box domotique) expose des MC pour envoyer des ordres à des périphériques Z-Wave :
Vous remarquerez d’ailleurs que les MC “SetDimmableLevel” et “SetSwitchState” prennent en argument un type complexe comme expliqué dans le chapitre précédent.
Vous remarquerez également que ces trois MC retournent un message de réponse, ici de type “Boolean”. En effet le résultat de l’exécution de l’ordre Z-Wave par la Vera est retourné à l’appellent si celui-ci à attaché un numéro de Saga.
Ainsi dans le code généré, les méthodes générées pour ces MessageCallbacks retournent une Task<T> où T est le type de retour, ici un booléen :
Ainsi dans le C# on peut très facilement invoquer le MessageCallback et récupérer la réponse :
1 2 3 4 5 6 7 8 9 |
bool result = await MyConstellation.Packages.Vera.CreateVeraScope().SetSwitchState(new DeviceRequest() { DeviceID = 42, State = false }); if (result) { PackageHost.WriteInfo("Device #42 turn off !"); } else { PackageHost.WriteWarn("Unable to turn off the device #42 !"); } |
Pour plus d’information à ce sujet, je vous recommande la lecture de l’article : Invoquer un MessageCallback avec réponse.
Démarrez la discussion sur le forum Constellation