Contôle à distance des ordinateurs allumés

vendredi 4 octobre 2013
par  Finizi
popularité : 33%

Ayant des enfants à la maison, je me bat régulièrement pour qu’ils éteignent leur PC quand il ne sont pas derrière l’écran.
Nous allons voir en quelques lignes de code comment obtenir au fil de l’eau, dans une page de son navigateur, l’état de fonctionnement d’un PC.
En effet, il s’agira d’une page web dynamique. L’état du PC surveillé sera automatiquement reporté sur la page web en cas de changement, et ce, sans avoir à recharger la page complète.


 Le principe

Sur un Raspberry Pi qui sert ici de serveur web, lancer un processus qui, à intervalle régulier, toutes les 65 secondes dans l’exemple ci-dessous, va envoyer un ping au PC à surveiller. L’absence de retour permettra de considéré le PC comme éteint.
La programmation du serveur va se faire en Python. Deux fichiers seront utilisés :

  • myPing.py, qui implémente le ping
  • webPing.py, qui fait tourner le server web

Coté client, ce sera une page HTML et un fichier JavaScript de quelques lignes. Le plus beau, c’est que le tout tient dans seulement 27 lignes. Il y a aussi 2 images.

La page web, via une mise en oeuvre particulière de Ajax, va régulièrement demander au serveur web l’état du PC à surveiller.
Toute la magie va reposer sur l’utilisation du framework AngularJS qui va encapsuler toute la plomberie Ajax XmlHTTP qui permet de rendre une page web dynamique.


 Implémentation du ping

Cela est fait dans le fichier myPing.py.
L’implémentation du ping se fait dans 2 classes :

  • La classe Ping dans laquelle seront définis les paramètres du ping, ainsi que le résultat du ping.
  • La classe readPing qui va exécuter le ping régulièrement en tâche de fond.

La classe Ping
Elle définie les propriétés suivantes :

  • "ip" : qui spécifie l’adresse IP de l’appareil auquel envoyer les pings.
  • "delay" : qui indique l’intervalle de temps entre chaque ping.
  • "isOn" : qui vaut 0 ou 1 et indique donc l’état de l’appareil surveillé.
  • "status" : qui est équivalent à "isOn" mais qui vaut "ON" ou "OFF".

Les propriétés "ip" et "delay" sont en lecture seule. Elles sont initialisées à l’instanciation de la classe par le constructeur.
La propriété "isOn" est modifiable, car c’est le processus indépendant chargé d’envoyer les pings qui modifiera cette propriété.
La propriété "status" est en lecture seule. Elle reflète la propriété "isOn".

La classe readPing
C’est elle qui effectue le ping en asynchrone, en lançant son propre processus.
J’aurais aimé faire le ping en pur Python, mais l’utilisation des socket réclame apparemment des droits élevés. Du coup, je me suis rabattu sur la commande système. Et pour ne pas être pollué par le texte affiché dans la console, j’envoie le tout dans "/dev/null".

response = os.system("ping -c 1 " + self._ping.ip + " >> /dev/null")

Le fichier Python complet est disponible en fin d’article et sur GitHub.


 Python et le serveur web

Il est implémenté dans le fichier webPing.py.

Pour servir les pages web, comme pour Suivi de la consommation électrique dans un navigateur web, j’ai décidé d’utiliser Bottle, principalement pour sa simplicité d’utilisation.

Chaque page web est définie par une fonction « décorée », c’est à dire que l’on a placé un décorateur sur la ligne précédent la définition de la fonction. En gros, cette décoration est utilisée par Bottle pour gérer toute la plomberie d’une page web.

Ici, le serveur web va servir 2 types de données différents :

  • Des données statiques, qui sont des fichiers de tout type, qui ne sont pas modifiés par le serveur. Ici, ce sont les fichiers :
    • isPcOn.html : la page web
    • pingCtrl.js : le script qui rend l’affichage dynamique
    • ONpc.png : l’image à afficher lorsque le PC est allumé
    • OFFpc.png : l’image à afficher quand le PC est éteint
  • Des données dynamiques http://127.0.0.1/json/pc. Le serveur ne renvoie que l’information permettant de connaitre l’état du PC. Cette information est au format JSON, qui est un format léger d’échange de données sous forme de texte.

Outre la fonction par défaut qui renvoie la page principale, il y a donc les 2 fonctions suivantes :

@webApp.route('/static/<fileName>')
def staticPages(fileName):
        return static_file(fileName, root='.')
@webApp.route('/json/<what>', method='ANY')
def json(what):
        json = {}
        if (what == "pc"):
                json = {"ip": pc.ip, "status": pc.status}
        response.headers['Cache-Control'] = 'no-cache'
        response.headers['Pragma'] = 'no-cache'
        response.headers['Expires'] = '-1'
        return(json)

Les lignes response.headers... sont là pour spécifier au navigateur client de na pas mettre en cache cette page de données. En effet, elle est susceptible de changer n’importe quand.

Et voilà, le serveur web est construit.

  • 1. La fonction staticPages(fileName) ne fait que renvoyer le fichier demandé qui se trouve dans le dossier "static".
  • 2. La fonction json(what) renvoie un dictionnaire contenant 2 informations :
    • Le statut du PC : « OFF »
    • L’adresse IP du PC : « 192.168.1.5 » dans cet exemple

Le fait de renvoyer un dictionnaire force Bottle à formater les données en JSON.

Voici un exemple du contenu renvoyé :

{"status": "OFF", "ip": "192.168.1.5"}

L’instanciation du site web doit se faire avant les fonctions par

webApp = Bottle()

A la fin du code est instanciée la classe Ping puis les site est lancé :

pc = myPing.Ping("192.168.1.5", 65)
# Lance le site web
run(webApp, host='0.0.0.0', port=8080, server='cherrypy')

Une dernière petite astuce : Bottle n’est pas un serveur en lui même, mais un outil permettant de programmer facilement un serveur HTTP. Il est possible d’utiliser le serveur par défaut. Toutefois, ayant remarqué des temps de réponse assez important, j’ai choisi d’utiliser CherryPy qui contient un serveur HTTP rapide et multitâche (paramètre “server” de la fonction run).

Le fichier Python complet est disponible en fin d’article et sur GitHub.


 HTML & Javascript

Nous arrivons maintenant à la partie affichage des données. L’information qui va être affichée est ici sous forme graphique. Il y a une image qui indique que le PC est en route, et une autre image, grisée, qui montre un PC éteint.
C’est en jouant sur le nom des images, nom qui doit contenir le statut (ON ou OFF) que l’on obtient l’affichage de l’une ou l’autre image en fonction du statut du PC.
PNG - 30.2 ko ou PNG - 8.3 ko
En utilisant certaines caractéristiques des feuilles de style CSS, il est sans doute possible de n’utiliser qu’une seule image, et de la modifier à la volée.

Le code HTML
Commençons par parcourir le fichier "isPcOn.html" qui ne contient que du code HTML.
Il est des plus simple : Il ne fait qu’afficher une image. Mais il recèle quelques subtilités afin de pouvoir exploiter AngularJS.

  • La première ligne <!doctype html>est nécessaire pour que le fichier s’affiche correctement dans Internet Explorer.
  • Dans le seconde ligne, la balise <html ng-app> contient "ng-app". L’attribut ng-app permet d’initialiser AngularJS. A n’utiliser qu’une seule fois dans une page HTML.
  • Pour pouvoir utiliser AngularJS, il est bien entendu nécessaire de charger la bibliothèque correspondante : <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.2/angular.min.js" type="text/javascript"></script>
  • Ensuite, il faut indiquer le script particulier à cet exemple <script src="static/pingCtrl.js" type="text/javascript"></script>.
  • Le corps de la page html ne contient qu’une image (balise img). Mais cette balise contient des attributs assez particuliers :
    • ng-controller="pingCtrlPc" : L’attribut ng-controller indique la classe JavaScript qui va contrôler toutes les données qui se trouve dans la balise <img ...>. Il est tout à fait possible par exemple de surveiller plusieurs PC. Il suffira de changer le nom du contrôleur, et de prévoir les codes Python et JavaScript en conséquence. ng-controller n’est actif que dans la balise dans laquelle il est déclaré. On peut le déclarer dans la balise <body>, mais dans ce cas, on ne pourra en déclarer qu’un seul dans tout le document.
    • ng-init="getJson()" : L’attribut ng-init indique la fonction JavaScript à appeler pour avoir les données.
    • id="pingCtrlPc" : Ne sert que pour Internet Explorer.
    • ng-src="static/{{data.status}}pc.png" : Habituellement, on trouve src, qui indique l’url de l’image à afficher. Mais comme cette url est ici dynamique, on utilise un élément AngularJS nommé ng-src. Ensuite, AngularJS va remplacer le texte "{{data.status}}" justement pas le statut renvoyé. Si le PC est allumé, on obtient ainsi pour source de l’image "static/ONpc.png". Je trouve cela magique !

Le fichier HTML complet est disponible en fin d’article et sur GitHub.

Le code Javascript
Il ne fait que définir la classe qui va servir de contrôleur. Cette classe est composée de :

  • la propriété "statut" qui correspondra au "status" renvoyé en JSON.
  • la propriété "ip" qui sera l’adresse IP renvoyée en JSON.
  • la fonction "getJson()" qui se charge de la communication avec le serveur web en l’interrogeant toutes les 2 secondes (120000 ms) sur l’url "json/pc" afin d’avoir le statut du PC.

Le fichier pingCtrl.js complet est disponible en plus bas et sur GitHub.


 Les fichiers complets

Le code est aussi disponible sur GitHub.

myPing.py
Le code qui effectue le ping :

  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3.        
  4. import os
  5. import sys
  6. import threading
  7. import time
  8.        
  9. class Ping(object):
  10.         """
  11.         Definir a quelle adresse IP envoyer les ping, et selon quel intervalle de temps.        
  12.         Renvoi le resultat
  13.         """
  14.        
  15.         def __init__(self, ip, delay):
  16.                 self._ip = ip
  17.                 self._delay = delay
  18.                 self._isOn = 0
  19.                 self.start()
  20.        
  21.         def _get_isOn(self):
  22.                 return(self._isOn)
  23.         def _set_isOn(self, newStatus):
  24.                 self._isOn = newStatus
  25.         isOn = property( fget = _get_isOn,      fset = _set_isOn, doc = "1 or 0" )
  26.        
  27.         def _get_ip(self):
  28.                 return(self._ip)
  29.         ip = property( fget = _get_ip, doc = "adresse IP a laquelle envoyer un ping" )
  30.        
  31.         def _get_delay(self):
  32.                 return(self._delay)
  33.         delay = property( fget = _get_delay, doc = "intervalle entre chaque ping, en secondes" )
  34.        
  35.         def _get_status(self):
  36.                 if (self._isOn == 1):
  37.                         return('ON')
  38.                 else:
  39.                         return('OFF')
  40.         status = property( fget = _get_status, doc = "On ou Off" )
  41.        
  42.         def start(self):
  43.                 pingReader = readPing(self)
  44.                 pingReader.start()
  45.                 time.sleep(1)                           # 1 seconde au demarrage, le temps d'avoir une reponse
  46.        
  47.        
  48. class readPing(threading.Thread):
  49.         """
  50.         Effectue un ping.
  51.         """
  52.        
  53.         def __init__(self, Ping):
  54.                 threading.Thread.__init__(self)
  55.                 self.setDaemon(True)
  56.                 self._ping = Ping
  57.        
  58.         def run(self):
  59.                 while True:
  60.                         response = os.system("ping -c 1 " + self._ping.ip + " >> /dev/null")
  61.                         if (response == 0):
  62.                                 self._ping.isOn = 1
  63.                         else :
  64.                                 self._ping.isOn = 0
  65.                         time.sleep(self._ping.delay)
  66.        
  67. #Exemple
  68. if __name__ == '__main__':
  69.         mp = Ping("192.168.1.5", 20)
  70.         while 1:
  71.                 print(mp.ip + ' ' + mp.status)
  72.                 time.sleep(30)

webPing.py
Le serveur web :

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3.        
  4. import myPing
  5. from bottle import Bottle, run, template, static_file, response
  6. import json as jsonModule
  7. import urllib2
  8.        
  9. ################################################
  10. #
  11. # Instancie un site web
  12. #
  13. webApp = Bottle()
  14.        
  15. ################################################
  16. #
  17. # Defini les pages du site web
  18. #
  19. @webApp.route('/')
  20. def homePage():
  21.         return static_file('static/isPcOn.html', root='.')
  22.        
  23. @webApp.route('/static/<fileName>')
  24. def staticPages(fileName):
  25.         return static_file(fileName, root='.')
  26.        
  27. @webApp.route('/json/<what>', method='ANY')
  28. def json(what):
  29.         json = {}
  30.         if (what == "pc"):
  31.                 json = {"ip": pc.ip, "status": pc.status}
  32.         response.headers['Cache-Control'] = 'no-cache'
  33.         response.headers['Pragma'] = 'no-cache'
  34.         response.headers['Expires'] = '-1'
  35.         return(json)
  36.        
  37. ################################################
  38. #
  39. # Instancie les classes de lectures des informations en arrière plan
  40. #
  41. pc = myPing.Ping("192.168.1.5", 65)
  42.        
  43. # Lance le site web
  44. run(webApp, host='0.0.0.0', port=8080, server='cherrypy')

isPcOn.html
La page HTML :

  1. <!doctype html>
  2. <html ng-app>
  3.   <head>
  4.     <meta charset="utf-8" />
  5.     <title>Is Pc On ?</title>
  6.     <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.2/angular.min.js" type="text/javascript"></script>
  7.     <script src="static/pingCtrl.js" type="text/javascript"></script>
  8.   </head>
  9.   <body >
  10.     <img ng-controller="pingCtrlPc" ng-init="getJson()" id="pingCtrlPc" ng-src="static/{{data.status}}pc.png" alt="" />                
  11.   </body>
  12. </html>

pingCtrl.js
Le code JavaScript :

  1.  function pingCtrlPc($scope, $http, $timeout) {
  2.         $scope.statut = '--';
  3.         $scope.ip = '--';
  4.         $scope.getJson = function() {
  5.                 $http({method: 'GET', url: 'json/pc'}).
  6.                 success(function(data, status) {
  7.                         $scope.status = status;
  8.                         $scope.data = data;
  9.                 }).
  10.                 error(function(data, status) {
  11.                         $scope.status = '??';
  12.                 });
  13.                 $timeout($scope.getJson, 120000);
  14.         };
  15. }

Le fichier archive

GZ - 39.4 ko
Exemple complet

Télécharger le fichier archive contenant l’ensemble des fichiers, avec les 2 images utilisées.
Ceci n’est qu’un exemple de ce qu’il est possible de faire d’une part en Python, et d’autre part en HTML, et plus particulièrement en utilisant le framework AngularJS qui simplifie énormément toute la plomberie permettant de rendre les pages web dynamiques.


Navigation

Articles de la rubrique