Envoi de SMS par un robot

Le principe, les outils

Je désire envoyer un SMS à tous les adhérents de mon association, pour leur communiquer une date d’un évènement sportif. Le mail ne fait plus d’effet ; il n’est pas lu, pas ouvert, pas arrivé…

J’opte pour un petit programme qui enverra en masse les SMS par mon smartphone. L’outil SMSGatewayMe me propose cette solution, gratuite, avec une API en PHP et une application à installer sur mon Smartphone.

 

La solution en détails

Depuis mon PC, mon programme (= mon robot) contacte par WebService la plateforme SMSGatewayMe, avec des messages du type « Envoyer le texto ‘Salut, il y a un barbecue ce week-end. tchao ! (association AAA)’ à ’06.12.34.56.78′, depuis mon smartphone, id=13245 ».

La plateforme SMSGatewayMe stocke cette info dans sa base de données, avec l’id du smartphone, le message, le destinataire.

L’application sur mon smartphone contacte régulièrement la plateforme SMSGatewayMe : « Est-ce qu’il y a des SMS à envoyer pour le smartphone id=12345 ?« . Elle lui retourne en réponse le SMS à envoyer.

L’application sur mon smartphone crée un nouveau SMS, visible depuis mes messages de mon smartphone. Les personnes me répondent directement.

 

La technique

Après avoir créé un compte sur https://smsgateway.me/, il faut récupérer le fichier « smsGateway.php ». Regardons le programme « run.php » :

<?php
include "smsGateway.php";

function main(){
  $smsGateway = new SmsGateway('moi-meme@gmail.com', 'monMot2Passe');
  $deviceID = 12345;

  $message = "Salut,
il y a un barbecue ce week-end.
tchao !
(association AAA)";

  $row = 1;
  if (($handle = fopen("envoi-sms.csv", "r")) !== FALSE) {
    while (($data = fgetcsv($handle, 1000, ";")) !== FALSE) {
      //echo "----". $data . "\n";
      $nbChamps = count($data);
      //echo "$nbChamps champs dans la ligne #$row\n";
      if($nbChamps < 2) {
        echo "fini\n";
        return;
      }
      $user = $data[0];
      $adresse = $data[1];
      $tel = $data[2];
      if( $user == "Nom" ){
        //echo "ligne d'entete\n";
        continue;
      }
      echo "Envoi d'un SMS vers ".$user." --> ".$tel."\n";
      $result = $smsGateway->sendMessageToNumber($tel, $message, $deviceID);

      $row++;
    }
    fclose($handle);
  }
  echo "--\n";

}

main();
?>

Le fichier « envoi-sms.csv » est un tableur Excel exporté en CSV, avec les colonnes suivantes : « Nom », « Adresse », « Téléphone ». Il y a une ligne d’entête pour mieux se repérer.

Enfin, une fois que tout est prêt, il suffit de lancer la commande suivante :

php run.php

 

Remarques

Ok, l’évènement n’est pas très sportif, passons…

Le nombre de caractères est limité. Faites des tests pour vous assurer que les sauts de ligne sont correctement placés, les caractères spécieux sont bien interprétés, le compte est bien configuré, etc.

Application Back-office & Mobile : CodeIgniter & Kivy

Introduction

Après quelques essais avec Kivy pour faire une application mobile qui indique des données d’infusion sur les thés, je me lance pour faire une application en back-office pour gérer les données. L’application mobile récupère les données en JSON et stocke dans une base SQLite.

Voici les outils utilisés de chaque coté :

Back-office Appli Mobile
Générateur de code Écriture du code « à la main »
PHP, CodeIgniter Python, Kivy
MySQL SQLite3

 

Principe général :

 

Application Back-office

Avec le générateur de code (plus d’infos sur ce site), l’application est créée, déployée et les données ajoutées, le tout en 1 heure sur « free.fr ». Le plus long a été de copier / coller les données entre ma source (le site du palais des thés) et ma nouvelle application.

Le controller fournissant le flux JSON est lui aussi généré :

Les écrans n’ont pas été retravaillés suite à la génération de code, mais c’est suffisant pour tout faire fonctionner sur l’environnement de « free.fr » : http://jc.specs.free.fr/TeaTime/ (un identifiant et mot de passe sont nécessaires…). Le nouveau thème Ubuntu (en gris et orange a été choisi).

Écrans du back-office

Catégorie de thés :

Thés :

Application mobile

Avec Kivy, je disposais déjà d’une application qui lit un JSON sous forme de fichier. Il m’a suffit de lui permettre de lire des fichiers JSON par Internet, de remplir une base de données SQLite et de la remplir.

Voici les étapes de réalisation :

  1. Lire dans un processus parallèle le JSON. Au lieu d’attendre que l’URL réponde, les widgets de l’écran de l’appli mobile sont disponibles.
  2. Stocker le JSON dans une base SQLite embarquée avec l’application. Lorsque c’est fait, un widget de l’application est modifié : un label devient « Fichier chargé ».
  3. Lorsque l’utilisateur entre dans les écrans de l’appli mobile, il faut lire la base de données SQLite et valoriser les objets qui seront utilisés par Kivy.

Kyvi n’est d’aucune utilité pour réaliser ces opérations de synchronisation ; Python prend le dessus : gestion des processus asynchrones, lecture d’une URL, transformation de JSON en objet, accès à une base de données SQLite.

 

Écrans de l’appli mobile

   

Le fichier APK de l’application mobile est téléchargeable sur http://jc.specs.free.fr/TeaTime/

 

Test unitaires dans le générateur de code

Après avoir intégré avec succès les tests unitaires dans CodeIgniter avec Toast, j’ai ajouté un template de test unitaire dont l’objectif est de s’assurer que la couche modèle fonctionne. Ces tests sont indépendants de la BDD métier car ils utilisent une base SQLite3.

Pour ce faire, Toast permet de créer un controller dédié à cette tâche.

J’ai choisi une entité qui s’appelle « Groupe » et qui a 3 propriétés « grpidgrp = Identifiant », « grplblib = Libellé », « grpidsoc = Clé étrangère vers l’entité Société ». Voici en détail les tests réalisés (automatiquement générés avec le code de l’application, donc).
(Tout ce code est généré, rien n’a été retouché)

/*
 * Created by generator
 *
 */
require_once(APPPATH . '/controllers/test/Toast.php');

class GroupeTest extends Toast {

	function __construct(){
		parent::__construct();
		$this->load->database('test');

		$this->load->model('Groupe_model');

	}

	/**
	 * OPTIONAL; Anything in this function will be run before each test
	 * Good for doing cleanup: resetting sessions, renewing objects, etc.
	 */
	function _pre() {
		$groupes = Groupe_model::getAllGroupes($this->db);
		foreach ($groupes as $groupe) {
			Groupe_model::delete($this->db, $groupe->grpidgrp);
		}
	}

	/**
	 * OPTIONAL; Anything in this function will be run after each test
	 * I use it for setting $this->message = $this->My_model->getError();
	 */
	function _post() {
		$groupes = Groupe_model::getAllGroupes($this->db);
		foreach ($groupes as $groupe) {
			Groupe_model::delete($this->db, $groupe->grpidgrp);
		}
	}

	public function test_insert(){
		$this->message = "Tested methods: save, getGroupe, delete";
		// création d'un enregistrement
		$groupe_insert = new Groupe_model();
		// Nothing for field grpidgrp
		$groupe_insert->grplblib = 'test_0';
		$groupe_insert->grpidsoc = 0;
		$groupe_insert->save($this->db);
		// $groupe_insert->grpidgrp est maintenant affecté

		$groupe_select = Groupe_model::getGroupe($this->db, $groupe_insert->grpidgrp);

		$this->_assert_equals($groupe_select->grpidgrp, $groupe_insert->grpidgrp);
		Groupe_model::delete($this->db, $groupe_select->grpidgrp);
	}

	public function test_update(){
		$this->message = "Tested methods: save, update, getGroupe, delete";
		$groupe_insert = new Groupe_model();

		// Nothing for field grpidgrp
		$groupe_insert->grplblib = 'test_0';
		$groupe_insert->grpidsoc = 0;
		$groupe_insert->save($this->db);

		// Nothing for field grpidgrp
		$groupe_insert->grplblib = 'test1_0';
		$groupe_insert->grpidsoc = 90;
		$groupe_insert->update($this->db);

		$groupe_update = Groupe_model::getGroupe($this->db, $groupe_insert->grpidgrp);

		if(!$this->_assert_equals($groupe_insert->grpidgrp, $groupe_update->grpidgrp)) {
			return false;
		}
		if(!$this->_assert_equals($groupe_insert->grplblib, $groupe_update->grplblib)) {
			return false;
		}
		if(!$this->_assert_equals($groupe_insert->grpidsoc, $groupe_update->grpidsoc)) {
			return false;
		}

		Groupe_model::delete($this->db, $groupe_insert->grpidgrp);
	}

	public function test_count(){
		$this->message = "Tested methods: getCountGroupes, save, getGroupe, delete";

		// comptage pour vérification : avant
		$countGroupesAvant = Groupe_model::getCountGroupes($this->db);

		// création d'un enregistrement
		$groupe = new Groupe_model();
		// Nothing for field grpidgrp
		$groupe->grplblib = 'test_0';
		$groupe->grpidsoc = 0;
		$groupe->save($this->db);

		// comptage pour vérification : après insertion
		$countGroupesApres = Groupe_model::getCountGroupes($this->db);

		// verification d'ajout d'un enregistrement
		$this->_assert_equals($countGroupesAvant +1, $countGroupesApres);

		// recupération de l'objet par son  grpidgrp
		$groupe = Groupe_model::getGroupe($this->db, $groupe->grpidgrp);

		// suppression de l'enregistrement
		Groupe_model::delete($this->db, $groupe->grpidgrp);

		// comptage pour vérification : après suppression
		$countGroupesFinal = Groupe_model::getCountGroupes($this->db);
		$this->_assert_equals($countGroupesAvant, $countGroupesFinal);

	}

	function test_list(){
		$this->message = "Tested methods: save, getAllGroupes, delete";

		$groupe_insert = new Groupe_model();
		// Nothing for field grpidgrp
		$groupe_insert->grplblib = 'test_0';
		$groupe_insert->grpidsoc = 0;
		$groupe_insert->save($this->db);

		$groupes = Groupe_model::getAllGroupes($this->db);
		if( ! $this->_assert_not_empty($groupes) ) {
			return FALSE;
		}
		$found = 0;
		foreach ($groupes as $groupe) {
			if($groupe->grpidgrp == $groupe_insert->grpidgrp &&
					$this->_assert_equals($groupe->grplblib, $groupe_insert->grplblib ) &&
					$this->_assert_equals($groupe->grpidsoc, $groupe_insert->grpidsoc )
				){
				$found++;
			}
		}
		if( $found == 1 ){
			Groupe_model::delete($this->db, $groupe->grpidgrp);
			return TRUE;
		}else{
			return FALSE;
		}
	}

}

 

Il faut récupérer le script SQLite3 généré et l’intégrer dans la BDD de TU :

/*
 * Lancer la commande suivante pour insérer les données
 * cat cretab_groupe.sqlite | sqlite3 test_database.sdb
 */

CREATE TABLE expgrp (
	grpidgrp integer NOT NULL PRIMARY KEY AUTOINCREMENT ,
	grplblib varchar(255) NOT NULL ,
	grpidsoc integer NOT NULL
);

 

Dans un navigateur, voici le résultat sur l’URL du controller de TU :

A ce stade, aucune ligne de code n’a dû être écrite par un développeur. Il pourra commencer son application avec des bases saines. J’ose le terme de « Framework Métier », permettant de poser les briques du système à réaliser.

 

Test unitaires avec CodeIgniter

Introduction

Les tests unitaires, tout le monde connait. En Java, junit fait l’unanimité (en plus, il est intégré à Eclipse ce qui en fait une arme redoutable). En PHP, avec le framework CodeIgniter, beaucoup de choses sont proposées, mais sans tutoriel pour montrer comment ça fonctionne concrètement.

Voici ma méthode, avec PHPUnit, Toast et CodeIgniter v2. Bien sûr, ça n’a pas fonctionné du premier coup (ce serait trop facile). Les prérequis sont :

 

Il faut commencer par revoir l’installation de Toast

Juste un petit mot sur Toast : il est basé sur les classes de test unitaires du framework de Codeigniter (qui s’appuie sur PHPUnit pour les tests unitaires) ; il est composé de quelques fichiers seulement (facile à maintenir et à customiser) ; il permet de se connecter à une base données indépendantes, juste pour les tests. C’est donc un bon candidat pour faire ces tests unitaires.

  • Les fichiers Toast sont dans « monProjet/application/controllers/test/ » pour les contrôleurs (qui feront les tests) et dans « monProjet/application/views/test/ » pour le rendu graphique.
  • Fichier « Toast.php » : la classe doit être déclarée pour utiliser CodeIgniter v2 et il faut recâbler les accès au nom de la classe de test :
abstract class Toast extends CI_Controller {
  ...
  function __construct(){
		parent::__construct();
		$this->load->library('unit_test');
		$this->modelname = get_class($this); //$name;
		$this->modelname_short = get_class($this); //basename($name, '.php');
		$this->messages = array();
	}
  ...
}
  • Idem avec le fichier « Toast_all.php » :
class Toast_all extends CI_Controller {
  ...
  function __construct(){ 
      parent::__construct(); 
  }
  ...
}

 

Le premier test unitaire basique peut commencer

  • Créez un nouveau fichier « SimpleTest.php » :

Il faut préfixer les méthodes de test par « test_ » et seul la fin du nom de la méthode sera présentée dans le rendu graphique.

require_once(APPPATH . '/controllers/test/Toast.php');

class SimpleTest extends Toast {

	function __construct(){
		parent::__construct(__FILE__);
	}

	public function test_basic(){
		$my_var = 2 + 2;
		$this->_assert_equals($my_var, 4);
	}

	function test_false(){
		// Test code goes here
		$my_var = true;
		$this->_assert_false($my_var);
	}
}

Ce code est celui de Toast. Je pense faire une version plus correcte, mais ce sera pour plus tard…

 

Voir le résultat du test

Le test unitaire est vu comme un contrôleur du projet. Il suffit d’y accéder par son URL, dans son navigateur favori :

http://localhost/monProjet/index.php/test/simpletest

Le résultat est une page web qui présente les tests réalisés :

On peut cliquer sur le nom de la classe ou sur le nom de la méthode pour limiter l’affichage. Toast propose aussi un lancement de tous les tests disponibles.

Par la suite, on peut imaginer automatiser le lancement de ces tests avec un « wget » pour lancer le test suivi d’un « grep FAILED » pour filtrer le résultat. Je me demande si un rendu XML ou JSON peut être réalisé…

 

La suite

Pour clôturer ce sujet, je ferai un VRAI test unitaire dans mon projet, maintenant qu’on sait comment ça fonctionne. Si vous avez des idées de customisation du rendu graphique, postez-les !

J’intègrerai aussi certainement des tests unitaires dans mon générateur de code afin de pondre aussi des tests tout prêts.

 

Time Counted Dev : Design & deploy

J’ai un peu de temps pour faire un déploiement complet d’une application en partant du début, avec l’outil qui génère du code PHP.

  • 18h40 : j’ai lancé Eclipse, je vérifie que mon serveur web et MySQL sont toujours en marche : http://localhost/phpmyadmin/index.php
  • 18h41 : je vais sur http://jc.specs.free.fr/ pour designer les entités de mon application (je choisis specs v2 car il est le plus abouti)
  • 18h44 : je crée l’application « Biblo-BD » : une bibliothèque d’albums de BD
  • 18h45 : en attentant que Free.fr réponde, je configure le projet sous Eclipse :
    • vue PHP : projet / nouveau « bibloBD »
    • je reprend le ZIP du template d’application (: fichiers de CodeIgniter, vue de connexion, helper pour les dates SQL et JS+CSS sympatiques) et je décompresse tout ca dans mon répertoire eclipse
    • F5 sous Eclipse pour tout mettre à jour
    • 18h51 : je configure la base de données depuis le fichier « application/config/database » : $db['default']['database'] = « bibliobd »;
  • 18h53 : je repasse dans specs v2 pour définir les entités. Cette étape peut être relativement longue, surtout s’il y a beaucoup d’entités…
  • 19h07 : 4 entités sont créées (plutôt simple, avec des dépendances entre elles) :
    • Album (id, ref externe, dessinateur, scénariste, genre)
    • Auteur (id, nom, prénom, nationalité)
    • Genre (id, libellé)
    • Utilisateur (id, login, mot de passe) — juste pour se connecter…
  • 19h09 : je crée la base de données en local avec phpMyAdmin : « bibliobd » (avec tous les paramètres par défaut)
  • 19h10 : je génère le XML des entités (fichiers à télécharger), en créant un répertoire « sources » et j’y colle les 4 fichiers (+ un F5 dans Eclipse)
  • 19h12 : je jette un oeil sur les fichiers XML et je vérifie que la coloration du formatage XML est propre
  • 19h13 : je configure le générateur : j’ouvre le fichier « theme.cfg » pour changer quelques valeurs :
    • « theme=InternetDreams » : ce sera le thème visuel de l’application
    • « outDirFor_Classes=/home/julien/workspace/biblioBD/application/ » : le répertoire de destination des fichiers générés
    • « database=bibliobd »
    • « generate=all » : génère tous les types de fichiers
  • 19h16 : je lance la commande qui génère les fichiers :
    ./generate.py ../biblioBD/sources/*.xml
  • 19h19 : je fais un F5 dans Eclipse : je contrôle les fichiers générés
  • 19h20 : j’ouvre les fichiers SQL générés et je colle les ordres SQL de création de tables dans phpMyAdmin. Puis je lance les ordres de contraintes de clé étrangères.
  • 19h25 : je vais configurer le serveur web : /etc/apache2/conf.d/bilbiobd.conf
  • 19h29 : redémarrage d’apache
  • 19h30 : tentative de connexion : http://localhost/bibliobd/
  • 19h30 : configuration de la connexion web : fichier « application/config/config.php »
  • 19h32 : modification du contrôle d’accès : « application/controllers/welcome.php » (: faire en sorte qu’on arrive sur la page qui liste les BD). Par défaut, avec le compte « admin/admin », on se connecte.
  • 19h44 : navigation dans le code pour voir si tout est en place
  • 19h45 : utilisation de l’application pour créer des entités et voir si elles sont répercutées dans les autres écrans (saisie de l’URL directement)
  • 19h46 : modification des liens du menu par défaut afin de proposer des liens sur la gestion des entités : « application/helpers/views_helper.php », fonction « htmlNavigation »
  • 19h50 : contrôle des pages

Il reste à définir les règles spécifiques à chaque cas et la gestion des utilisateurs.

Résultat : 1h10 pour mettre en ligne une application de gestion complète, avec les liens entre les entités.

 

Optimisations possibles :

  • générer automatiquement le menu (opération complexe — la mise à jour manuelle est plus rapide)