Sommaire
Préparer la page AngularJS
Vous pouvez soit utiliser le gestionnaire de package Nuget depuis Visual Studio pour installer la dernière version du package “Constellation.Angular” et ses dépendances :
Ou bien utiliser (ou copier en local) les scripts des CDN en ajoutant dans votre code HTML les balises suivantes :
1 2 3 4 5 |
<script type="text/javascript" src="https://code.jquery.com/jquery-2.2.4.min.js"></script> <script type="text/javascript" src="https://ajax.aspnetcdn.com/ajax/signalr/jquery.signalr-2.2.2.min.js"></script> <script type="text/javascript" src="https://cdn.myconstellation.io/js/Constellation-1.8.2.min.js"></script> <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script> <script type="text/javascript" src="https://cdn.myconstellation.io/js/ngConstellation-1.8.2.min.js"></script> |
Dans votre code Javascript, vous devez créer un module Angular pour votre page que nous appelleront “MyDemoApp” et dans lequel nous injecterons le module “ngConstellation” :
1 |
var myDemoApp = angular.module('MyDemoApp', ['ngConstellation']); |
Vous devez également ajouter l’attribut “ng-app” sur la balise <html> de votre page pour lier votre page HTML à votre module Angular :
1 |
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="MyDemoApp"> |
Enfin vous devez créer un contrôleur AngularJS dans lequel nous injecterons la factory “constellationConsumer” que nous appellerons dans notre code “constellation” :
1 2 3 |
myDemoApp.controller('MyController', ['$scope', 'constellationConsumer', function ($scope, constellation) { }]); |
Sans oublier de lier votre corps de page (<body>) à ce contrôleur :
1 |
<body ng-controller="MyController"> |
Et voilà votre squelette est prêt !
Pour plus d’information sur AngularJS, je vous recommande la lecture de ce guide : https://docs.angularjs.org/misc/started
Pour résumer notre squelette page est donc :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" ng-app="MyDemoApp"> <head> <title>Test API AngularJS</title> <script type="text/javascript" src="https://code.jquery.com/jquery-2.2.4.min.js"></script> <script type="text/javascript" src="https://ajax.aspnetcdn.com/ajax/signalr/jquery.signalr-2.2.2.min.js"></script> <script type="text/javascript" src="https://cdn.myconstellation.io/js/Constellation-1.8.2.min.js"></script> <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script> <script type="text/javascript" src="https://cdn.myconstellation.io/js/ngConstellation-1.8.2.min.js"></script> <script> var myDemoApp = angular.module('MyDemoApp', ['ngConstellation']); myDemoApp.controller('MyController', ['$scope', 'constellationConsumer', function ($scope, constellation) { }]); </script> </head> <body ng-controller="MyController"> </body> </html> |
Connecter une page HTML à Constellation avec AngularJS
Dans notre contrôleur AngularJS, nous allons initialiser le client Consumer en spécifiant l’URL de votre serveur Constellation, la clé d’accès et le “Friendly name” de votre page :
1 |
constellation.initializeClient("http://localhost:8088", "123456789", "TestAPI"); |
Vous pouvez créer une clé d’accès depuis la Console Constellation ou en éditant le fichier de configuration du serveur.
Enfin ajoutons un handler sur le changement d’état de la connexion pour afficher le message “Je suis connecté” dans la console de votre navigateur par exemple lorsque la connexion à Constellation est établie :
1 2 3 4 5 |
constellation.onConnectionStateChanged(function (change) { if (change.newState === $.signalR.connectionState.connected) { console.log("Je suis connecté !"); } }); |
Il ne reste plus qu’à lancer la connexion en invoquant la méthode “connect” :
1 |
constellation.connect(); |
Et voilà, votre page AngularJS est connectée à votre Constellation.
Envoyer des messages et invoquer des MessageCallbacks
Envoyer des messages
Pour envoyer des messages et donc invoquer des MessageCallbacks vous devez utiliser la méthode “constellation.sendMessage” en spécifiant :
- Le scope
- La clé du message
- Le contenu du message (= les paramètres du MessageCallback)
Par exemple, avec le package Nest déployé dans votre Constellation, on retrouvera un MessageCallback “SetTargetTemperature” pour piloter la température de consigne d’un thermostat Nest.
N’hésitez pas à utiliser le MessageCallback Explorer pour découvrir l’ensemble des MessageCallbacks exposés par les packages de votre Constellation.
Pour invoquer le MessageCallback “SetTargetTemperature” depuis votre page Web en passant en paramètre l’ID du thermostat et la température de consigne :
1 |
constellation.sendMessage({ Scope: 'Package', Args: ['Nest'] }, 'SetTargetTemperature', 'my_thermostat_id', 19); |
En AngularJS, pour lier l’invocation de ce code à un bouton, vous devez simplement ajouter l’attribut “ng-click” sur votre bouton.
Par exemple, dans votre code HTML :
1 2 3 |
<body ng-controller="MyController"> <button ng-click="SetNestTemperature()">Set Nest to 19°C</button> </body> |
Et dans votre contrôleur :
1 2 3 |
$scope.SetNestTemperature = function () { constellation.sendMessage({ Scope: 'Package', Args: ['Nest'] }, 'SetTargetTemperature', 'my_thermostat_id', 19); }; |
Autre solution, exposer le client Consumer dans le scope AngularJS :
1 |
$scope.constellation = constellation; |
Et donc s’en servir directement dans le template HTML :
1 2 3 |
<body ng-controller="MyController"> <button ng-click="constellation.sendMessage({ Scope: 'Package', Args: ['Nest'] }, 'SetTargetTemperature', 'my_thermostat_id', 19)">Set Nest to 19°C</button> </body> |
Allons un peu plus loin en ajoutant un champ permettant à l’utilisateur de définir la température de consigne.
Dans le template HTML :
1 2 |
Temperature : <input type="number" ng-model="targetTemperature" /> <button ng-click="SetNestTemperature()">Set target temperature</button> |
Le champ “input” est de type “number” et est lié à la variable de scope (le modèle) que nous appellerons “targetTemperature”.
De ce fait dans notre fonction “SetNestTemperature” nous pouvons récupérer la valeur définie par l’utilisateur :
1 2 3 |
$scope.SetNestTemperature = function () { constellation.sendMessage({ Scope: 'Package', Args: ['Nest'] }, 'SetTargetTemperature', 'my_thermostat_id', $scope.targetTemperature); }; |
Comme pour l’API Javascript, vous pouvez également passer plusieurs arguments à votre MC combinant type simple et type complexe. Si il y a plusieurs paramètres vous devez les stocker dans un tableau.
Par exemple le MC “ShowNotification” du package Xbmc permettant d’afficher une notification sur une interface Kodi/XBMC prend deux paramètres : le nom d l’hôte XBMC (un type string) et le détail de la notification à afficher (un type complexe) :
1 |
constellation.sendMessage({ Scope: 'Package', Args: ['Xbmc'] }, 'ShowNotification', xbmcName, { "Title":"Hello", "Message":"Hello from JS" }); |
Bien entendu vous pouvez invoquer des MessageCallbacks de n’importe quel package, réel (programme Linux/Windows) ou virtuel (Arduino/ESP, scripts, etc..) ou même sur d’autres consommateurs (pages Web par exemple).
Ainsi vos pages Web peuvent, en envoyant des messages, invoquer des méthodes sur n’importe quel système connecté dans votre Constellation.
Par exemple dans l’article sur les ESP/Arduinos, notre ESP8266 exposait un MC “Reboot” pour redémarrer la puce. Si l’on souhaite redémarrer notre Arduino/ESP depuis une page Web, il suffirai d’envoyer un message sans paramètre au bon scope. Par exemple :
1 |
constellation.sendMessage({ Scope: 'Sentinel', Args: ['MyArduino'] }, 'Reboot'); |
Ici le scope est “Sentinel” avec l’argument “MyArduino”, c’est à dire que le message “Reboot” sera envoyé à tous les packages de la sentinelle “MyArduino”.
Les scopes peuvent être :
- Sentinel
- Package
- Group
- Others
- All
La propriété “Args” de l’objet scope est un tableau contenant le nom des groupes, des sentinelles ou packages en fonction du type de scope sélectionné. Seuls les scopes “All” et “Others” n’ont pas besoin d’argument.
Envoyer des messages avec réponse : les sagas
Les Sagas permettent de lier des messages et donc de créer des couples de “requêtes / réponses”.
Avec la libraire JavaScript, il est possible d’envoyer des messages dans une saga et d’enregistrer un callback pour traiter la réponse de ce message.
Par exemple, le package “NetworkTools” expose un MC “Ping” permettant de réaliser un ping réseau. Le résultat du ping est retourné dans un message de réponse (un Int64 représentant le temps du réponse du ping) :
De ce fait pour réaliser un ping depuis une page Web, on pourrait proposer à l’utilisateur un champ pour saisir l’adresse à pinger et afficher le résultat dans un paragraphe.
1 2 3 4 5 |
<body ng-controller="MyController"> Host/IP : <input ng-model="host" /> <button ng-click="Ping()">Ping</button> <p ng-show="pingResult">Result : {{pingResult}}ms</p> </body> |
Vous remarquez que le paragraphe du n’est affiché (ng-show) que si la valeur de scope “pingResult” est définie, ce qui n’est pas le cas à l’ouverture de la page.
Déclarons enfin la méthode de scope “Ping” pour envoyer un message au package “NetworkTools” afin d’invoquer dans une saga sur le MC “Ping” avec en paramètre du message variable « $scope.host » qui est liée à la zone de texte (input) de votre vue HTML.
Comme il s’agit d’une saga, nous utilisons la méthode “sendMessageWithSaga” dans laquelle nous passons la fonction callback qui sera invoquée lors de la réponse. A la réception cette réponse, nous allons tous simplement stocker le résultat de la réponse (propriété Data) dans la variable de scope “pingResult”.
L’affectation de la réponse dans la variable de scope “pingResult” est réalisée dans un “$scope.$apply”, une fonction AngularJS qui permet d’indiquer au contrôleur AngularJS que des variables de scope ont été modifiées et donc qu’il faut rafraîchir la vue.
1 2 3 4 5 6 7 |
scope.Ping = function () { constellation.sendMessageWithSaga(function(response) { $scope.$apply(function() { $scope.pingResult = response.Data; }); }, { Scope: 'Package', Args: ['NetworkTools'] }, 'Ping', $scope.host); }; |
Ainsi vos pages Web peuvent invoquer des méthodes et exploiter la réponse de tous les systèmes connectés sur Constellation exposant des MessageCallbacks.
Recevoir des messages
Une page Web connectée sur Constellation peut elle même recevoir des messages et donc exposer des MessageCallbacks que d’autres consommateurs (autres pages Web) ou packages (réels et virtuels) pourront invoquer.
Ceci dit un consommateur tel qu’une page Web n’a pas réellement d’existence. Autrement dit elle n’a pas d’identité et donc ne peut recevoir que des messages adressés à un groupe qu’elle devra joindre.
Pour joindre un groupe vous devez utiliser la méthode “subscribeMessages“ (et “unSubscribeMessages” pour en sortir). Vous pouvez joindre autant de groupe que vous souhaitez.
Par exemple, pour ajouter votre page au groupe “Demo” :
1 |
constellation.subscribeMessages("Demo"); |
Vous pourrez ensuite recevoir les messages envoyés dans ce groupe en ajoutant un handler sur le “onReceiveMessage” :
1 2 3 |
constellation.onReceiveMessage(function (message) { console.log("Message recu !", message); }); |
Dans ce handler vous recevrez TOUS les messages (quelque soit la clé du message).
Vous avez aussi un moyen plus simple consistant à définir un handler pour une clé de message spécifiquement via la méthode “registerMessageCallback”.
Par exemple :
1 2 3 4 5 6 |
constellation.registerMessageCallback("ChangeTitle", function (msg) { document.title = msg.Data; }); constellation.registerMessageCallback("HelloWorld", function (msg) { alert("Hello World"); }); |
Ici on enregistre deux MessageCallbacks.
Si votre page reçoit un MessageCallback “HelloWorld”, une alerte sera ouverte, si elle reçoit un MessageCallback “ChangeTitle”, le titre de votre page sera modifié avec l’argument passé dans le MC.
Bien entendu, comme vos pages HTML peuvent enregistrer autant de MessageCallbacks qu’elles souhaitent et que ces MessageCallbacks sont invocables depuis n’importe quel système connecté à Constellation, d’autres pages Web, des objets à base d’Arduino, ESP ou autre, des programmes Python, .NET, etc… vous pouvez imaginer tout type d’usage.
Consommer des StateObjects
Pour consommer des StateObjects produits par des packages (virtuels ou réels) de votre Constellation, vous avez deux méthodes : l’évènement “onUpdateStateObject” ou les StateObjectLinks.
La première méthode consiste à enregistre un ou plusieurs handlers sur l’évènement “onUpdateStateObject” :
1 2 3 |
constellation.onUpdateStateObject(function (stateobject) { console.log(stateobject); }); |
Les handlers seront déclenchés à chaque mise à jour ou interrogation des StateObjects de votre page quelque soit le StateObject.
Pour interroger un ou plusieurs StateObjects à un instant T vous disposez de la méthode “requestStateObjects”.
Pour vous abonner aux mises à jour d’un ou plusieurs StateObjects, vous disposez de la méthode “subscribeStateObjects” (et “unSubscribeStateObjects” pour vous désabonner).
Enfin la méthode “requestSubscribeStateObjects” réalise un “requestStateObjects” et un “subscribeStateObjects” c’est à dire que la méthode demande la valeur actuelle du ou des StateObjects et s’abonne auprès du serveur Constellation pour être notifiée des mises à jour.
Toutes ces méthodes prennent en paramètre : le nom de la sentinelle, le nom du package, le nom du StateObject et le type du StateObject. Vous pouvez utiliser le wildcard “*” pour ne pas appliquer de filtre (plus d’information ici).
Par exemple, pour récupérer tous les StateObjects du package “Demo” quelque soit la sentinelle et pour s’abonner aux mises à jour de ces StateObjects :
1 |
constellation.requestSubscribeStateObjects("*", "Demo", "*", "*"); |
L’autre méthode consiste à enregistrer un (ou plusieurs) StateObjectLink. Cela permet de lier une fonction à un abonnement.
Par exemple :
1 2 3 4 5 6 7 |
constellation.registerStateObjectLink("*", "HWMonitor", "/intelcpu/0/load/0", "*", function (so) { // Code A }); constellation.registerStateObjectLink("*", "Demo", "*", "*", function (so) { // Code B }); |
Ci-dessus le code A sera invoqué dès qu’un SO nommé « /intelcpu/0/load/0” et produit par le package “HWMonitor” est modifié alors que le code B sera déclenché dès qu’un StateObject du package “Demo” est modifié.
Prenons un cas pratique, nous voulons afficher en temps réel la consommation de notre CPU. Comme expliqué ci-dessus, la consommation peut être connue avec le StateObject nommé « /intelcpu/0/load/0” et produit par le package “HWMonitor”.
Nous allons donc ajouter un StateObjectLink et affecter la variable de scope “cpu” à chaque mise à jour de ce SO. Comme pour la réception de message, nous devons utiliser la fonction Angular “$scope.$apply” pour informer le contrôleur Angular que nous avons changé une variable de scope :
Aussi vous devez enregistrer vos SOLink une fois connecté :
1 2 3 4 5 6 7 8 9 |
constellation.onConnectionStateChanged(function (change) { if (change.newState === $.signalR.connectionState.connected) { constellation.registerStateObjectLink("*", "HWMonitor", "/intelcpu/0/load/0", "*", function (so) { $scope.$apply(function() { $scope.cpu = so.Value.Value; }); }); } }); |
Et dans notre template :
1 2 3 |
<body ng-controller="MyController"> <p>CPU : {{cpu}}</p> </body> |
Plutôt que stocker dans notre variable de scope la propriété “Value” de la valeur de notre StateObject, nous pouvons aussi stocker le StateObject lui même :
1 |
$scope.cpu = so; |
De ce fait dans le template nous pouvons afficher différentes informations contenu dans notre StateObject :
1 2 3 |
<body ng-controller="MyController"> <p>CPU on {{cpu.SentinelName }} at {{cpu.LastUpdate}} is <strong>{{cpu.Value.Value}} {{cpu.Value.Unit}}</strong></p> </body> |
Alors bien sûr le format de la date ou encore la précision de la valeur mesurée par le package HWMonitor ne sont très lisibles. Nous pouvons ajouter des filtres AngularJS directement dans notre template pour rendre notre page plus “user-friendly”.
Utilisons le filtre “date” pour formater la date et “number” pour limiter le nombre de chiffre après la virgule :
1 |
<p>CPU on {{cpu.SentinelName }} at {{cpu.LastUpdate | date : 'dd//MM/yyyy @ hh:mm:ss' }} is <strong>{{cpu.Value.Value | number:2}} {{cpu.Value.Unit}}</strong></p> |
C’est toute la magie d’AngularJS
Allons encore un peu plus loin !
Ici nous avons lié notre variable de scope “cpu” au StateObject nommé « /intelcpu/0/load/0” et produit par le package “HWMonitor”. Mais n’oubliez pas que l’unicité d’un StateObject est obtenu avec le triplet : Sentinelle + Package + Nom.
Autrement dit il peut y avoir plusieurs SO qui correspondent à ce filtre dans le cas où vous avez déployé le package HWMonitor sur plusieurs sentinelles.
Dans ce cas, la valeur du SO est sans cesse écrasée et notre page devient illisible :
Nous allons gérer ce cas en créant une variable de scope qui contiendra les différents StateObjects de nos CPUs.
Dans votre scope, commençons par déclarer une variable de scope nommée “cpus” en l’initialisant avec un objet vide :
1 |
$scope.cpus = {} |
Modifions notre StateObjectLink pour stocker le StateObject dans notre objet “cpus” sous la propriété du nom de la sentinelle de notre StateObject.
1 2 3 4 5 |
constellation.registerStateObjectLink("*", "HWMonitor", "/intelcpu/0/load/0", "*", function (so) { $scope.$apply(function() { $scope.cpus[so.SentinelName.replace("-", "")] = so; }); }); |
C’est à dire que notre variable de scope “cpus” est un objet avec des propriétés du nom de la sentinelle et qui contient le StateObject de son CPU associé.
Attention le nom de la sentinelle est utilisée comme nom de propriété de notre objet JavaScript et vous n’êtes pas sans savoir que les “-“ (entre autre) ne sont pas autorisés en JS ce qui explique que nous les effaçons avec le “replace”.
Ainsi si je veux afficher dans mon template le CPU de ma machine nommée ”PC-SEB_UI” (= propriété “PCSEB_UI”), je devrais écrire :
1 |
<p>CPU on {{cpus.PCSEB_UI.SentinelName }} at {{cpus.PCSEB_UI.LastUpdate | date : 'dd//MM/yyyy @ hh:mm:ss' }} is <strong>{{cpus.PCSEB_UI.Value.Value | number:2}} {{cpus.PCSEB_UI.Value.Unit}}</strong></p> |
Il ni a plus d’effet indésirable de voir les StateObjects chevaucher constamment.
Mais on peut aller encore plus loin car notre variable “cpus” contient tous les SO de la consommation de CPU produits par l’ensemble des packages HWMonitor de notre Constellation.
Il suffit d’itérer sur cette variable pour afficher le SO de chaque propriété de notre objet. Avec AngularJS vous pouvez utiliser la directive “ng-repeat” :
1 |
<p ng-repeat="cpu in cpus">CPU on {{cpu.SentinelName }} at {{cpu.LastUpdate | date : 'dd//MM/yyyy @ hh:mm:ss' }} is <strong>{{cpu.Value.Value | number:2}} {{cpu.Value.Unit}}</strong></p> |
Nous obtenons une page capable d’afficher en temps réel la consommation de toutes nos machines Windows dans une simple page HTML :
Démarrez la discussion sur le forum Constellation