lundi 14 mai 2012

Développement plugin de A à Z pour HomeAutomate Pachube/Cosm Partie 2

     Passons maintenant à la seconde étape, l'écriture du code JAVA nous permettant de réaliser la remontée automatique. 


     1ère phase: 


     Pachube/Cosm mettent à disposition une API Java (JPachube) téléchargeable ici. Malheureusement, cette API ne fait pas partie du repository MAVEN, dommage donc pour les dépendances dans le pom. J'ai donc décidé de l'incorporer dans le plugin en tant que tel (Les sources étant téléchargeables).

On se retrouve donc avec un package Pachube. Au passage j'ai modifié quelques classes afin d'améliorer le parsing SAX qui parfois fait un peu des siennes.






     2ème phase:


Explication de la classe CosmManager. Première chose à faire implémenter l'interface IUnitAware qui forcera notre objet à implémenter la méthode :


public void onEvent(IUnit unit)

      Donc pour finir, à chaque fois qu'une Unité (THN132, ou autre) au sens large du terme est mise à jour (par un médiateur), le système va invoquer cette méthode dans tous les objets "Aware". Notre CosmManager recevra donc en paramètre à chaque Event et comme par magie l'unité mise à jour.



Voici le programme : 


Et voilà, le programme faire 130 lignes de codes (sans les getter/setter).
Light non ?

Quelques explications sur le fonctionnement :

       A chaque event reçu, si l'unité est concernée (On peut paramétrer les unités à Feeder), on stocke alors la valeur de l'unité (récupérée à l'aide de getValue()) dans une Map. On a pris soin 'd'overrider' la méthode doTraitement() propre à chaque Manager. Cette méthode ne servira qu'à ouvrir les vannes pour les remontées. doTraitement() est appelé tous les x milliseconds appelé le heartBeat.

Le manager a donc trés peu d'attributs : 

      - Une liste d'unité¨s dont on souhaite remonter l'information.
      - Une apiKey, fournie par Cosm.
      - Un booléen pour permettre de remonter ou non les infos à chq battement de coeur du manager.
      - Une liste d'unités ayant fait l'objet d'événements.

Vous noterez l'annotation Editable avant chaque attribut. Cela permettra au plugin Reflexion de proposer ces attributs en configuration, on y reviendra dans la partie 3.



Notre manager est prêt, il faut maintenant faire un export sous forme de jar et placer le résultat dans notre répertoire de plugins.


Développement plugin de A à Z pour HomeAutomate Pachube/Cosm Partie 1

Passons maintenant aux choses sérieuses. L'écriture d'un plugin pour HomeAutomate.
Ce plugin aura pour but de remonter automatiquement les valeurs des différentes unités vers Cosm.

On commence donc sous notre IDE préféré : Eclipse Java.

Après s'être connecté au SVN Homeautomate on descend le projet template plugin. On va donc faire un simple copier/coller du projet :




Ici, je le nomme donc HomeAutomateCosmPlugin. Jusque là rien de difficile.
Regardons maintenant l'arborescence de notre nouveau projet, on va faire un peu de ménage... et paramétrer quelques fichiers.

Notre plugin sera en fait un manager 'Aware' : à l'écoute de n'importe quelle modification faite sur une unité (ici nos THN132 ou autres Unit peu importe). 






Etre Aware se concrétisera dans notre cas par l'implémentation d'une interface IUnitAware. (Rappel il existe également un IMediatorAware). Commençons donc par supprimer les packages qui ne nous intéressent pas (mediator & unit).







Ensuite, on regarde ce qu'il y a dans notre package manager, une classe MyManager est déjà présente.



Renommons donc cette classe en CosmManager. Et passons à la configuration de 2 fichiers :
    
       pom.xml : permettra de spécifier les dépendances maven avec d'autres          
                      librairies.
       plugin.xml : fichier SPRING déclarant les beans SPRING et leurs injections   
                      de dépendances.

Commençons par le POM : Rien de plus simple quelques balises à compléter, le fichier étant déjà configuré pour les dépendances.


   Vous noterez la dépendance ci dessous vers HomeAutomateCore, indispensable pour être reconnu par le système.






   Puis enfin, le fichier plugin.xml, déjà rempli partiellement, il faudra juste mettre  à jour quelques balises et indiquer en rouge le package et notre classe CosmManager.



   Il est également possible de déposer un petit logo du plugin sous resource (même endroit que plugin.xml). Le logo devra s'appeler bien entendu cosm.png du nom de la valeur indiquée dans imagePath dans le fichier plugin.xml



   Voilà pour la première partie. Rien de bien méchant. On a donc un projet sous Eclipse s'appelant HomeAutomateCosmPlugin, avec 1 seule classe à l'intérieur CosmManager.



dimanche 13 mai 2012

Pachube devient Cosm





      Etant inscrit sur Pachube depuis pas mal de temps, sans trop avoir à vrai dire utilisé cette fonctionnalité. Je me suis connecté récemment et j'ai pu constaté que Pachube était "routé" sur Cosm.

L'aperçu du site à l'air plus moderne, des exemples d'API, des graphs plus sympas, bref voici le site :

Tout commence en se signant, il est dans doute possible de récupérer les identifiants de Pachube, à vrai dire personnellement, je me suis recréé un nouveau compte :



Ensuite direction key, permettant d'obtenir une clé pour remonter les informations depuis vos systèmes.


Il est possible de créer des clés en lecture seule, écriture, bref tous les droits sont paramétrables, ici une clé full.



Ensuite, On passe aux "Feeds", on peut donc créer des Flux d'alimentation (Sondes, Twitter, Arduino)....
Biensûr, j'ai pris un truc custom (Something Else) :


Et j'ai donc créé les flux suivants pour test (Garage et Salon), on verra un dans autre <post>, que les flux n'ont pas été créés manuellement mais automatiquement par HomeAutomate :


Chaque flux peut recevoir plusieurs "courbes" appelés "datastream", il suffit donc d'adresser un id à chaque dataStream. (Ici l'id 0 entouré en rouge). Feed Id étant le super Id (celui qui regroupera tous les datastream)



On notera également la géolocalisation : 





On pourra récupérer les infos sous forme JSON, XML ou CSV. Il y a d'autres options permettant de réaliser des graphiques et de les importer à l'aide d'url dans vos sites.



Il existe une notion de trigger qui peut donc comme son nom l'indique : de déclencher une action si une valeur est atteinte ou autre condition. 2 options (POST HTTP ou TWITTER)




Voilà pour ce petit reportage, désolé mes graphs sont un peu vides, mais je vais attendre que cela se remplisse un peu.

Ah oui, et n'oubliez pas de faire un tour sur les API, très bien faites avec exemples : 


Pas mal de lecture sur les Arduino également :


Le site est en BETA, j'espère qu'il perdurera.


jeudi 10 mai 2012

Intéraction avec HomeAutomate avec URL

Un peu de Réflexion Java pour dialoguer en ligne de commande avec le système HomeAutomate.

Explication : Le but étant d'afficher les informations ou même d’exécuter des méthodes internes à l'aide de commandes URL. (Client en ligne de commande à venir).

Après avoir installé le plugin Réflexion HomeAutomate:






Pré-requis : 


                 J'ai donc configuré 1 device RfxcomEthernet, ainsi que 2 de mes sondes OREGON (après avoir installé les 2 plugins RfxCom & Oregon :






               Après avoir démarré HomeAutomate, on constate dans le Journal de Bord que notre RFXCOM commence à recevoir du message ...






Maintenant, l'objectif étant de récupérer les valeurs températures en interrogeant le système mais sans IHM !
Une Action Struts a donc été déployée dans HomeAutomate, permettant de questionner, intéragir avec les objets (Bean Spring) au sein du système.

Cette action est atteignable via une requête HTTP (Le futur client sera chargé de transformer les paramètres d'un programme en une requête HTTP).

Détails de la requête : 

ObjectAction?action=getUnits&objectName=&objectClass=systemManager&outputFormat=xml

ObjectAction = L'action permettant d’accéder au service d’interaction.

Les paramètres : 

     action : la méthode à invoquer. (Ici on invoque la méthode getUnits() )
    objectName : Permet de récupérer un objet précis (A utiliser dans certains cas, ici pas la peine)
  objectClass : Permet de préciser une Classe d'object prédéfinie (Vocal, System, ...) Si la classe n'est pas trouvée dans la liste définie, on ira alors chercher dans le contexteSpring et trouver le bean spring répondant à ce objectClass. Ici systemManager, signifie que l'on va récuperer le bean spring s'appellant 'systemManager'
    outputFormat(optionnel) : Permet de sérialiser le retour en xml.

 Pour résumer, je demande ici de récupérer toutes les unités (donc les Thn132) connues du système.

Que se passe-t-il en background ? Java va aller chercher l'objet systemManager et rechercher par réflexion la méthode getUnits(), une fois trouvée, il va invoquer la méthode sur l'objet systemManager et sérialiser le retour de la méthode en xml.

Démonstration : 

L'url avec votre navigateur préféré : 




Le résultat (en retour de requête HTTP) en rouge, le nom des sondes et les températures.


     
La sérialisation est effectuée avec une librairie excellente appelée XStream, vraiment trés simple d'utilisation.
XStream xs = new XStream();

textStream = new StringBufferInputStream(xs.toXML(objet));




Voici un petit panel d'exemple de requêtes (avec possibilité de passer des paramètres aux méthodes)



/**
 * <code>
 * Ex:
 * Permet de faire parler le système, ici le paramètre arguments permet de passer les arguments à la méthode  talk qui prends en paramètre le texte à parler. le [null] permet de spécifier la classe du paramètre, permet de  rechercher la méthode adhoc.


 * ObjectAction?action=talk&objectName=&objectClass=vocalManager&arguments=Essai[null]
 * 
 * Permet ici de récuperer toutes les pièces déclarées
 * ObjectAction?action=getAllRoomsName&objectName=&objectClass=houseManager
 *  
 * Permet ici de récuperer toutes les résidences déclarées
 * ObjectAction?action=getAllHousesName&objectName=&objectClass=houseManager
 * 
 * Permet ici de récuperer toutes les résidences déclarées, le prefixe et suffixe, permettent d'enrober chaque ligne par une balise ou autre chaine. utile pour les attributs de type liste.
 * ObjectAction?action=getAllHousesName&objectName=&objectClass=houseManager&prefixe=<option>&suffixe=</option>
 * 
 *  Permet de récupérer tous les managers.
 * ObjectAction?action=getManagers&objectName=&objectClass=systemManager&outputFormat=xml
 * 
 * ObjectAction?action=getUnits&objectName=&objectClass=systemManager&outputFormat=xml
 * 
 * Permet ici de récupérer un plugin à l'aide de son petit nom passé en arguments.
 * ObjectAction?action=getPluginByName&objectName=&objectClass=System&outputFormat=xml&arguments=HomeAutomateReflexionPlugin






     On remarquera lors de la sérialisation que certains attributs (Manager, dao) ne sont pas sérialisés, ils sont en fait déclarés comme transcient car dynamiques. Ils ne doivent pas être sauvegardés en base ni même en xml.


        Il est également possible de récupérer des Streams de type image. En effet , si l'attribut à récupérer est une image ou de l'affichable de type ByteArrayInputStream. L'action se chargera de transformer le retour en image.


mercredi 2 mai 2012

Pas facile de trouver un hébergeur gratuit de WAR

       



Pas facile de trouver un hébergeur gratuit de WAR Full JAVA. Heureusement avec le Cloud, de nombreux services apparaissent et permettent gratuitement de disposer de JVM gratuites, d'espaces gratuits de stockage, et d’innombrables services complémentaires.

Hébergeur WAR CLOUDBEES :



    Ce site vous permettra de déposer 5 WEBAPP de manière gratuite. Certe la mémoire max sera limitée à 128M, mais c'est déjà pas mal. Au chapitre des services tous aussi gratuits : 

       - Possibilité de configurer une base de données MYSQL (Pour notre application, cela ne nous concerne pas trop, puisqu'une configuration DB4O a été retenue, on verra par contre l'inconvénient de CloudBees)

           -  Mise à Disposition d'outils d'analyse de votre code.

           -  Intégration continue avec Jenkins.

           -  Repository Maven, Subversion ..

           -  Intégration dans Eclipse.

           -  Total management de vos applications.

           -  Mise en sommeil de l'application

Accès à la console d'administration




2 possibilités pour déployer les wars :

               1) Dépose par upload du war (classique, mais lent tout dépend de votre taux de transfert en upload)


                   2) Compilation et déploiement depuis le dépot Cloudbees, utilisation du SDK à intégrer dans Eclipse (plus rapide, la compile et le déploiement se faisant à distance). Intéressant en cas de "fat" war.


      Inconvénient pour HomeAutomate. L'utilisation de DB4O comme base de données Objets nécessite la présence d'un filesystem permanent. Or, avec CloudBees, le filesystem et purement volatile. La JVM disposera au démarrage d'un filesystem mais qui sera complètement purgé au redémarrage de l'application. On se voit donc mal y stocker le fichier DB4O.


      L'idée étant de disposer un espace de stockage gratuit sur le Cloud, nous permettant de stocker / récupérer un fichier de manière autonome. Le best-of serait qu'en plus cet outil dispose d'API JAVA permettant d'y accéder programmatiquement. Ainsi à chaque démarrage d'HomeAutomate à l'aide d'un properties embarqué, l'application irait chercher la dernière sauvegarde de la base. Une sauvegarde automatique serait également intégrée dans le SystemManager, permettant de sauvegarder à intervalles réguliers, ou en fonction de l'activité.



     Je suis donc tombé sur DROPBOX (Il y a également depuis peu GoogleDrive), DROPBOX semble plus robuste et plus expérimenté pour ce besoin. Biensur le compte est gratuit, on dispose de 2go nativement et totalement expandable via $$. Il existe des clients OS Windows, Linux et Mac. Sous windows, l'installation ajoute automatiquement un lecteur réseau où vous pouvez glisser/coller vos fichiers à sauvegarder (J'imagine même principe pour les autres).
          Cette fonctionnalité n'est pas transcendante, ce qui m'intéresse, ce sont les API proposées par DropBox pour les développeurs, on y retrouve donc tous les environnements du moments proposés :


Et donc celui qui nous intéresse c'est JAVA. Après différentes recherches dans le forum expliquant le mode d'authentification par systéme de clés publiques puis clés privées, voici l'explication. 

       Lorsque vous utilisez une API pour une application, vous déclarez l'application sur le site. Le site vous fournit alors une clé (TOKEN key) et un mot secret (TOKEN SECRET). Cela ne permet pas encore de sauvegarder ou récupérer le moindre fichier par l'application. Il faut appairer l'application afin de valider son accès définitif. Pour cela il faut se connecter et via l'API demander explicitement un couple de clés appairées (TOKEN_PAIR key + secret). Il faudra alors pendant la demande, passer ensuite sur le site DropBox et valider l'appairage. Cela parait peut être un peu lourdingue, mais une fois terminé, le site vous retourne votre couple d'appairage, que vous stockerez au chaud dans un properties et plus besoin de se réauthentifier (On utilisera alors les clés appairées).

Voici la classe utilisée pour générer un lien vers un compte DROPBOX, vous noterez les méthodes getFile et putFile assez simplistes. L'api propose également d'autres méthodes (Le main permet l'étape d'authentification)


package com.homeautomate.cloud.dropbox;


import java.awt.Desktop;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Properties;


import javax.swing.JOptionPane;


import com.dropbox.client2.DropboxAPI;
import com.dropbox.client2.DropboxAPI.Entry;
import com.dropbox.client2.exception.DropboxException;
import com.dropbox.client2.session.AccessTokenPair;
import com.dropbox.client2.session.AppKeyPair;
import com.dropbox.client2.session.RequestTokenPair;
import com.dropbox.client2.session.Session.AccessType;
import com.dropbox.client2.session.WebAuthSession;
import com.dropbox.client2.session.WebAuthSession.WebAuthInfo;




public class DropBox {


private static String APP_KEY = "";
public static String DEFAULT_PATH = "myApplication";
private static String APP_SECRET = "";
private static final AccessType ACCESS_TYPE = AccessType.APP_FOLDER;
private static DropboxAPI<WebAuthSession> mDBApi;
private static Boolean authentified = Boolean.TRUE;
private static String TOKEN_KEY = "";
private static String TOKEN_SECRET = "";


public static void main(String[] args) throws Exception {


AppKeyPair appKeys = new AppKeyPair(APP_KEY, APP_SECRET);
WebAuthSession session = new WebAuthSession(appKeys, ACCESS_TYPE);


if (authentified) {


WebAuthInfo authInfo = session.getAuthInfo();


RequestTokenPair pair = authInfo.requestTokenPair;
String url = authInfo.url;


Desktop.getDesktop().browse(new URL(url).toURI());
JOptionPane.showMessageDialog(null,
"Press ok to continue once you have authenticated.");
session.retrieveWebAccessToken(pair);


AccessTokenPair tokens = session.getAccessTokenPair();
System.out
.println("Use this token pair in future so you don't have to re-authenticate each time:");
System.out.println("Key token: " + tokens.key);
System.out.println("Secret token: " + tokens.secret);
} else {
session.setAccessTokenPair(new AccessTokenPair(TOKEN_KEY,
TOKEN_SECRET));
}


mDBApi = new DropboxAPI<WebAuthSession>(session);


System.out.println();
System.out.print("Uploading file...");
String fileContents = "Hello World!";
ByteArrayInputStream inputStream = new ByteArrayInputStream(
fileContents.getBytes());
// Testing
Entry newEntry = mDBApi.putFile("/testing.txt", inputStream,
fileContents.length(), null, null);
System.out.println("Done. \nRevision of file: " + newEntry.rev);


DropBox db = new DropBox();
File uploadFile = new File("C:/chat.jsp");
db.putFile("database", uploadFile);


File downloadFile = new File("C:/downloadFile.txt");
db.getFile("database/chat.jsp", downloadFile);


}


public Entry putFile(String path, File file) throws IOException,
DropboxException {
InputStream fis = new FileInputStream(file);
Entry newEntry = mDBApi.putFileOverwrite(DEFAULT_PATH + "/" + path,
fis, file.length(), null);
fis.close();
return newEntry;


}


public void getFile(String fileNameWithPath, File destinationFile)
throws IOException, DropboxException {
FileOutputStream fos = new FileOutputStream(destinationFile);
mDBApi.getFile(DEFAULT_PATH + "/" + fileNameWithPath, null, fos, null);
fos.close();
}


public DropBox() {
Properties properties = new Properties();
InputStream in = this.getClass().getResourceAsStream(
"/dropbox/dropbox.properties");


try {
properties.load(in);
APP_KEY = properties.getProperty("APP_KEY");
APP_SECRET = properties.getProperty("APP_SECRET");
TOKEN_KEY = properties.getProperty("TOKEN_KEY");
TOKEN_SECRET = properties.getProperty("TOKEN_SECRET");
} catch (IOException e) {
e.printStackTrace();
}
AppKeyPair appKeys = new AppKeyPair(APP_KEY, APP_SECRET);
WebAuthSession session = new WebAuthSession(appKeys, ACCESS_TYPE);
session.setAccessTokenPair(new AccessTokenPair(TOKEN_KEY, TOKEN_SECRET));
mDBApi = new DropboxAPI<WebAuthSession>(session);
}
}




Et voilà notre petit tour sur les nuages est terminé.

  
     


dimanche 11 mars 2012

Quelques news de HomeAutomate


Manque de temps, travaux , font que le développement passe en arrière plan.
Ceci dit voici quelques news :

      Gestion des plugins terminée, pour l'instant 4 plugins : 
                         
- Plugin RFXCOM Ethernet (Permet la reconnaissance de la partie HARDWARE d'un RFXCOM LAN)
- Plugin OREGON (Permet la reconnaissance des sondes OREGON)
- Plugin HeatManager (Permet de la gestion chauffage d'une pièce, étage, maison, réseau de maisons)
- Plugin Reflexion (Permet la configuration d'une Unité, Mediator, Manager) par réflexivité JAVA (Introspection dynamique dans les objets fournis par les plugins)


     

    Gestion Architecture (Pièce, Etage, Maison) terminée.

   
 

      Gestion des plages horaires terminée (Design à fignoler)



     IHM de réflexivité (permet de paramétrer de manière dynamique un plugin)
 

  Côté Software : 

            - Disparition d'Hibernate (la partie dynamique plugin, m'obligeant à m'en séparer)
            - Nouvel ORM et du coup base de données (et oui, je supprime MySql également) au profit de DB4O (Base de données objets) de manière hyper simpliste.

           Ex: DatabaseContainer.save(monObjet);

A venir : 
              
           Plugin Pachube

Puisque NEST n'est pas prêt d'être disponible en France







    Puisque le magnifique thermostat NEST n'est pas prêt d'être disponible en France avant pas mal de temps. Pourquoi ne pas se lancer dans la fabrication d'un thermostat virtuel type "Potar" pilotable en tactile sur nos tablettes.

Après quelques recherches, je suis tombé sur cette DEMO. Une fois décortiquée, je m'aperçois qu'il y a pas mal de variables en dur, et le simple fait de vouloir un potar plus gros, m'embarque dans des calculs de dingue. En plus cette démo inclut foultitude de css et de script js. Et je voulais également que le potard puisse tourner librement sans "butée".

Donc pourquoi pas simplifier et s'adapter. 

Liste des composants : 

     - JQuery (intégré dans HomeAutomate) et permettant de faire des IHM sympa.
     - Une image de potar (désolé pour la qualité)

       

     - Une fonte type LCD bleu néon.


On commence par intégrer le style (surtout les fontes) : 


<style>
a img { border: none;
   }
a { color: #FFF;
  }
@font-face {
    font-family: "FontLCD";
    src: url(fonts/DS-DIGI.TTF) format("truetype");
    }
p.lcdfont {
    font-family: "FontLCD", Verdana, Tahoma;
    font-size:85px;
    color:#5ac3f1;
}
</style>

Puis vient la partie JQuery, il faut importer JQuery Core et un plugin dédié aux rotations 
Ex : 


<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript" src="js/jQueryRotateCompressed.js"></script>

Enfin vient la partie script JQuery (y a surement moyen d'optimiser, je ne trouve pas fluide le défilement): 




<script type="text/javascript">
$(document).ready(function(){
// Variable pour le dernier angle calculé
var lastAngle=0;
// Variable pour la dernière position en abcisse
var lastx=0;
// Variable pour le dernier signe calculé
var lastSign="+";
// Variable pour savoir si le boutton de la souris est pressé.
var mouseDown="";
// température par défaut au commencement
var temperature=19;
$( "#temperature" ).html( "19.0°C");


$('#potar').mousedown(function(e){
  // Lorsqu'on appuie sur le bouton de gauche, on autorise le thermostat à bouger
  mouseDown="ok";
});

$('*').mouseup(function(e){
  // Lorsqu'on relache le bouton de gauche, on n'autorise plus le thermostat à bouger
  mouseDown="";
});


$('#potar').mousemove(function(e){
  // Si on est autorisé à bouger
  if ( mouseDown == "ok" )
   {
    var left=e.pageX- this.offsetLeft;
    var top=e.pageY - this.offsetTop;
    var width=$('#potar').width();
    var height=$('#potar').height();
    var sens="";
    if ( lastx < (left) && (height/2 - top ) > 0)
     {
      sens="+";
     }
    if ( lastx >= left && (height/2 - top ) > 0)
     {
      sens="-";
     }
   if ( lastx <= (left) && (height/2 - top ) < 0)
     {
      sens="-";
     }
    if ( lastx >= left && (height/2 - top ) < 0)
     {
      sens="+";
     }
// Si rien ne bouge on conserve le même signe
     if ( lastx == left  )
      {
       sens=lastSign;
      }
    // Si le sens est positif on fait tourner le disque, et on augmente la t°
    if ( sens == "+" )
      {
lastAngle+=1;
        temperature+=0.1;
      }
     else
     {
   lastAngle-=1;
        temperature-=0.1;
     }  

// Arrondi au Dixième
     temperature=Math.round(temperature*10)/10;

// Si le sens est indeterminable, on garde le dernier
     if ( sens != "" )
      {
     lastSign=sens;
      }
// On effectue une rotation
     $('#potar').rotateAnimation(lastAngle);
     lastx = left;
     
    $( "#temperature" ).html( temperature +"°C");
    }  
   
});
});
</script>

Puis la partie HTML : 


<body style="background: #000; color: #FFF;">
<p name="temperature" id="temperature" class="lcdfont" style="text-align: center;" ></div>
<div style="text-align: center;"><img  draggable="false" id="potar" src="img/potar.png" /></div>
</body>




Résultat : 




jquery knob rotate