Trois applications d’Asterisk à la maison

 x0r   0
sip asterisk

Je dois faire partie d’une infime minorité de gens qui, à mon âge, utilise le téléphone fixe et je dois bien faire partie d’une minorité encore plus infime en utilisant cette ligne fixe avec un système Asterisk à la maison.

Ce que j’avais à l’origine installé chez moi pour jouer s’est révélé être de plus en plus utile avec le temps. D’abord parce que feu mon projet siproxd_orange a été une excellente façon d’apprendre le SIP. Mais ensuite parce que mon infrastructure existante m’a permis de développer de petites applications pas forcément très compliquées, mais très utiles. Autrement dit, mon serveur Asterisk est passé du statut de solution en quête de problèmes à une solution pour de vrais besoins que j’ai fini par avoir plus tard.

J’ai pu en effet mettre en œuvre récemment quelques idées d’applications utiles et, de surcroît, appréciées par ma bien-aimée. Ces applications ont pour point commun d’exploiter une fonction que proposent certains téléphones SIP : le décroché automatique sur haut-parleur pour certains appels entrants. Dans ce billet, je vous en propose trois : un simple système d’interphone, un système de diffusion globales d’annonces vocales et un système de rappels automatique par diffusion globale.

Avant de commencer, je tiens à préciser que j’utilise PJSIP en faveur de l’ancien module chan_sip que je ne compile même plus dans mes propres installations d’Asterisk et qui est officiellement déprécié depuis Asterisk 19. Si vous utilisez encore chan_sip, à vous d’adapter mes exemples.

Interphone

Imaginons le scénario suivant : je viens de finir ma cuisine pendant que Madame est sur son PC en train de jouer. Pour l’appeler à table, je pourrais crier à travers l’appartement « À table ! », mais je n’ai pas envie de m’égosiller à chaque fois. Je pourrais aussi composer, depuis le téléphone de la cuisine, le numéro du poste à son bureau : mais alors, comme elle a les deux mains occupées, elle ne peut pas décrocher.

La solution serait ce que j’appellerai ici « interphone » par souci de concision : c’est-à-dire la possibilité pour moi d’appeler un poste et que cet appel soit automatiquement décroché sur haut-parleur.

Certains modèles de téléphones SIP déclenchent une prise automatique d’appel sur haut-parleur si un en-tête particulier se trouve dans le message INVITE, en général Alert-Info, et a une valeur particulière. En tout cas :

  • sur des Yealink W52P, il faut un Alert-Info égal à Auto answer ;
  • sur des Polycom SoundPoint IP 450, la valeur nécessaire est configurable, alors j’ai fait en sorte de s’aligner sur les Yealink.

Pour d’autres constructeurs, il y a peut-être besoin d’une valeur différente, d’un en-tête différente ou alors la fonction n’existe pas du tout.

Pour ajouter cet en-tête dans le message INVITE envoyé à l’appelé, il faut pouvoir positionner la variable PJSIP_HEADER(add,Alert-Info) du demi-appel (call leg) entre Asterisk et l’appelé. Le faire directement dans l’extension (i.e. faire un Set juste avant le Dial vers le destinataire) ne marchera pas, car cela agit sur la signalisation SIP entre l’appelant et Asterisk.

Il faut donc passer par une astuce de l’application Dial : l’option b permet d’indiquer vers quelle extension effectuer un Gosub dans le contexte du demi-appel entre Asterisk et l’appelé.

J’ai créé un contexte handlers, dans lequel je place des extensions qui ne permettent pas d’appeler des téléphones. L’extension dont j’ai besoin, que j’ai appelée set_auto_answer, est définie ainsi :

[handlers]

exten => set_auto_answer,1,NoOp()
 same => n,Set(PJSIP_HEADER(add,Alert-Info)=Auto Answer)
 same => n,Return()

Enfin, j’ai choisi de mettre en œuvre l’accès à ce service en préfixant le numéro du poste par *. Par exemple, 4042 déclenche un appel classique mais *4042 donne un appel d’interphone. Il ne reste plus qu’à déclarer l’extension qui me permet d’appeler un poste ainsi, comme ceci :

[phones]

exten => *4042,1,NoOp()
 same => n,Dial(PJSIP/foo,b(handlers^set_auto_answer^1),3)
 same => n,Hangup()

Le paramètre crucial ici étant l’option b(…) donnée à l’application Dial. Avec ceci, on devrait désormais avoir un interphone fonctionnel.

Une limitation de ce système que je trouve un peu frustrante, c’est que je n’ai pas d’indication claire du moment exact à partir duquel je peux parler. C’est d’autant plus problématique quand j’appelle depuis ou vers un téléphone Yealink, parce qu’ils sont lents.

Diffusion globale

Maintenant qu’on a une fonction d’interphone, on peut s’en servir pour en faire un système de diffusion globale, aussi appelée « paging » en anglais.

Cela consiste à définir un numéro qui, une fois composé, permet de diffuser une annonce audible dans un bâtiment entier (par exemple, dans un magasin) en parlant dans le combiné du téléphone.

Pour cela, il faut utiliser l’application Page au lieu de Dial, car Page crée une conférence ad hoc dans laquelle sont placés tous les téléphones appelés, alors que Dial aurait simplement provoqué la diffusion sur un téléphone au hasard. Les options sont très proches. J’ai choisi l’extension *4000 pour qu’il soit facile à mémoriser. Dans extensions.conf, voilà ce que ça donne :

[globals]

; Liste de tous les téléphones prenant en charge le décroché auto
PAGE_ALL_PHONES => PJSIP/foo&PJSIP/bar&PJSIP/baz&…

[phones]

exten => *4000,1,NoOp()
 same *> n,Page(${PAGE_ALL_PHONES},b(handlers^set_auto_answer^1)di,3)
 same => n,Hangup()

Je laisse trois secondes à tous les téléphones pour prendre l’appel. Ce délai permet de gérer correctement le cas d’un téléphone déjà en communication : dans ce cas, le téléphone sonne en double appel, mais cet appel est abandonné après quelques secondes chez ceux qui n’ont pas pu le prendre directement sur haut-parleur. J’ai procédé par tâtonnements pour trouver le délai idéal, surtout à cause de mes Yealink, qui mettent beaucoup de temps avant de réagir à un message INVITE. Un délai de 2 s s’est avéré insuffisant, par exemple, mais 3 s semble être plus fiable.

Je me donne la possibilité d’avoir du son bidirectionnel avec l’option d. Avec cette option, Asterisk mixe ensemble le son capté par l’ensemble des téléphones appelés et envoie le résultat à l’appelant, ce qui permet à l’appelé, où qu’il soit, de répondre comme pour un appel interphone classique. Attention cependant à l’effet Larsen quand on teste avec plusieurs combinés proches les uns des autres ou quand plusieurs postes se trouvent dans la même pièce !

Le résultat ne me permet que des annonces en direct. On pourrait imaginer une version plus avancée qui permette d’enregistrer un message, de le réécouter puis de diffuser ce message après validation, comme ce que permettent certains systèmes commerciaux, mais je n’ai jusqu’ici pas eu besoin d’une application aussi complexe.

Rappels automatiques

Des circonstances particulières font que ma bien-aimée a ressenti le besoin d’avoir un système de rappels à des heures fixes. Comme elle n’a pas et ne veut pas de smartphone, je me suis dit que je pouvais rapidement bricoler un système utilisant le service de diffusion globale que j’avais déjà avant : comme ça, au moins, je suis certain que le message atteindra sa cible à l’heure souhaitée.

Ma solution est mise en œuvre par une tâche cron, qui exécute un script shell, qui dépose dans le répertoire /var/spool/asterisk/outgoing un fichier d’appel (call file) spécial. Ce fichier décrit un appel émis par un pseudo-canal local vers le service de diffusion générale que j’avais déjà.

Tout se passe dans un contexte dédié que j’ai appelé reminder et qui se programme de la façon suivante :

[reminder]

exten => broadcast,1,NoOp()
 same => n,Page(${PAGE_ALL_PHONES},b(handlers^set_auto_answer^1)iq,3)

exten => play_message,1,NoOp()
 same => n,Answer()
 same => n,Wait(2)
 same => n,Playback(reminder)
 same => n,Hangup()

L’application Playback joue un fichier appelé reminder, préalablement enregistré par mes soins et placé dans le répertoire des sons d’Asterisk.

Ensuite, le fichier d’appel en question a le contenu suivant :

Channel: Local/play_message@reminder
Context: reminder
Extension: broadcast 
Callerid: Rappel automatique <reminder>

La variable Channel d’un fichier d’appel indique normalement le poste qu’Asterisk fait préalablement sonner. Une fois décroché, ce canal est alors envoyé vers le triplet Content, Extension et Priority donné en argument (et Priority vaut 1 par défaut). Dans notre cas, on utilise un canal « local », qui simule ici une personne ayant appelé le service de diffusion globale, attend deux secondes (pour laisser le temps aux Yealink de réagir) et qui parle dans son combiné avant de raccrocher.

Enfin, le script appelé par la tâche cron est assez simple. Une façon de faire (qui nécessiterait les droits root, donc l’idéal serait que ce soit fait sous un autre utilisateur non privilégié, éventuellement avec des droits sudo) serait de procéder ainsi :

install -m 600 -o asterisk -g asterisk \
        /chemin/vers/reminder.call \
        /var/spool/asterisk/_tmp_call && \
    mv /var/spool/asterisk/_tmp_call \
       /var/spool/asterisk/outgoing/reminder.call

L’écriture de ce genre de scripts exige une attention particulière car il y un piège : il y a en effet un risque de race condition entre le processus qui écrit le fichier d’appel et Asterisk qui traite un fichier d’appel dès qu’il apparaît dans le répertoire adéquat. Si on ne fait pas attention, Asterisk risque de traiter un fichier tronqué, vierge ou illisible pour cause de droits insuffisants et l’appel échouera. Pour éviter cela, il faut procéder en deux étapes :

  • écrire le fichier dans un répertoire proche, changer le propriétaire et le groupe en asterisk et le mode en 600 (pas besoin de plus) ;
  • quand le fichier est prêt, le déplacer (mv) dans le bon répertoire ; le déplacement d’un fichier étant atomique tant que le répertoire source et destination sont sur le même système de fichiers.

Ce système de rappels automatiques a ainsi fonctionné pendant plus d’un mois sans faillir (excepté au début, lors de sa mise au point, à cause du piège mentionné ci-dessus notamment).

Conclusion

J’ai montré comment construire pas à pas une petite gamme d’applications reposant sur le principe d’appels internes automatiquement pris sur haut-parleur.

La fonction d’interphone est très appréciée par ma compagne, parce qu’elle apporte une alternative au fait de hurler « C’est prêt ! » quand j’ai fini de faire la cuisine qui lui est beaucoup moins désagréable. La diffusion globale lui permet ensuite de n’avoir qu’un seul numéro à mémoriser. Enfin, le rappel périodique en diffusion globale lui a été très utile pendant plus d’un mois et lui sera encore d’une aide très précieuse pour les quelques mois à venir. Voilà donc un exemple concret d’une application intéressante pour un PBX à la maison !

Mais ce n’est pas tout : la souplesse de mon serveur Asterisk, et plus largement le fait d’avoir une infrastructure réseau chez moi qui ressemble peu ou prou à un réseau d’entreprise, m’ont bien servi pendant les épisodes de confinement dues à la pandémie du Covid-19. Mais ceci sera un sujet pour un autre billet de blog.

Commentaires

Poster un commentaire

Poster un commentaire