The OpenNET Project / Index page

[ новости/++ | форум | wiki | теги ]

Русскоговорящий будильник/напоминаловка для Asterisk
В Asterisk есть штатный будильник - напоминаловка, если вы загляните в
директорию /var/lib/asterisk/agi-bin, то увидите что-то похожее на wakeup.php.
Вещь не плохая, но хочу предложить Вашему вниманию, более продвинутую версию
русскоговорящего будильника. В нашем случае, Вы можете отправлять голосовые
сообщения как сами себе, так и на любые другие номера. Причем задавать дату
можно как в четком виде ГГММДД ЧЧММ, так и в неявном виде, через 10 минут от
текущего времени, или через 3-и дня от текущей даты.

Концепция будильника следующая, пользователь вводит со своего телефона
комбинацию цифр вида *0*X*Y*Z, где

X - дата, когда необходимо озвучить сообщение
Y - время, когда необходимо озвучить сообщение
Z - номер, на который необходимо позвонить для проигрывания сообщения


Возможные варианты значения X (дата, когда проигрывать напоминание)

0 - Звонить сегодня
От 1 до 99 - Позвонить через 1-99 дней
ГГГГММДД - Позвонить в конкретную дату

Возможные варианты значения Y (время, когда проигрывать напоминание)

0 - Звонить в это время (не работает с текущим днем)
От 1 до 999 - Звонить через 1-999 минут
ЧЧММ - Звонить в конкретное время

Возможные варианты значения Z (номер куда будем отправлять напоминание)

0 - Номер с которого звонят
zzz -Локальный номер (у меня 3-значные номера)
zzzzzzzzzzzzzzzzz - Внешний номер, может быть любым!

Примеры:

*0*0*120*0  - позвонить через 2-а часа  на свой телефон, и озвучить
надиктованное сообщение (не забыть выключить суп!).

*0*20101231*2300*777 - позвонить 31 декабря 2010 года в 23.00 на номер 777 и
озвучить поздравление с новым годом!

*0*30*1500*78123090607 - позвонить через 30 дней  в 15.00 на номер 78123090607
и озвучить сообщение (пора оплачивать счета)!

*0*20110308*0*1234567 - позвонить 8 марта 2011 года в текущее время на номер
1234567 и озвучить поздравление с праздником!

Думаю, концепция ввода даты, времени  и номера, куда необходимо позвонить для
напоминания вам ясна. Теперь перейдем к технической реализации нашей задачи.


В файле extensions.conf мы должны описать наш экстеншен!

   exten => _*0*X.,1,Answer
   exten => _*0*X.,n,AGI(reminder.agi)
   exten => _*0*X.,n,Hangup

Таким образом, все набранные номера, которые начинаются с *0* попадают в наш
контекст и включают скрипт reminder.agi

Далее опишем контекст go - он запускается call файлом в момент когда происходит
дозвон до абонента с целью озвучить надиктованное сообщение.

   exten => go,1,NoOp("Будильничег")
   exten => go,n,AGI(reminder_listen.agi)
   exten => go,n,HagnUp

ну и теперь самое интересное, это наши скрипты

/var/lib/asterisk/agi-bin/reminder.agi

   #!/usr/bin/perl
   use Asterisk::AGI;
   use POSIX;
   use File::Copy;
   use Time::Local;
   use Date::Calc qw (Add_Delta_Days);

   $AGI = new Asterisk::AGI;
   my %input = $AGI->ReadParse();

   $trunk="megatrank";
   # начало отсчета с 1900 года, значение year - смещение от 1900 года
   $datesec=time;
   #chomp($datesec);
   # получаем значение exten и заносим в переменную $digit - 
   # это то что набрал пользователь на своем телефоне, далее мы 
   # разберем по частям эти цифры с целью понять что хотел сказать 
   # пользователь (когда и куда звонить)
   $digit = $AGI->get_variable('EXTEN');
   # на всякий случай, выдергиваем номер звонящего
   $src = $AGI->get_variable('CALLERID(num)');
   #регулярным выражением выдергиваем значения даты времени и номера звонящего
   $digit =~ /^\*0\*(\d+)\*(\d+)\*(\d+)/;
   $wdate=$1;
   $wtime=$2;
   $wnum=$3;

   # вводим маленькую функцию для озвучки ошибки набора, что бы
   # несколько раз не писать один и тот же код!
   # обратите внимание, я взял звуковые файлы с http://ivrvoice.ru/downloader
   # и перекинул их  в Asterisk в папку /var/lib/asterisk/sounds/ru11 !
   sub digit_error {
      $AGI->exec('Wait',"1");
      $AGI->exec('Playback',"ru11/an-error-has-occured");
      $AGI->exec('Playback',"ru11/check-number-dial-again");
      exit 0;
   }
   # звуковые файлы в директории digits мною были заменены с 
   # английского на русский аналог, все с того же сайта. На самом 
   # деле это не совсем верно, по идее Астериск должен цеплять 
   # русские файлы после переопределения глобальной переменной LANG, 
   # однако сходу у меня не получилось, потому исправил ситуацию 
   # простым копированием файлов.
   # Данная функция, корректно для нашего чисто русского слуха, озвучивает день!
   sub say_day {
      $sayday=$_[0];
      if ("$sayday" < 20) {
         $AGI->exec('Playback',"digits/h-$sayday\\n");
      } elsif ( 20 < "$sayday" and  "$sayday" < 30 ) {
         $sayday=~/\d(\d)/;
         $AGI->exec('Playback',"digits/20");
         $AGI->exec('Playback',"digits/h-$1\\n");
      } elsif ( "$sayday" == 20 ) {
         $AGI->exec('Playback',"digits/h-20n");
      } elsif ( "$sayday" == 30 ) {
         $AGI->exec('Playback',"digits/h-30n");
      } else {
         $AGI->exec('Playback',"digits/30");
         $AGI->exec('Playback',"digits/h-1\\n");
      }
   }

   # берем локальное время и разбиваем на переменные
   ($sec2,$min2,$hour2,$mday2,$mon2,$year2,$wday2,$yday2,$isdst2)=localtime($datesec);
   # не забываем к году прибавить 1900 а к месяцу единичку
   $tmpyear2=$year2+1900;
   $tmpmon2=$mon2+1;
   # для проверки работы скрипта выводим на экран информацию, когда у нас задание стартовало.
   $AGI->exec('NoOp',"Старт_задания_$tmpyear2.$tmpmon2.$mday2\_в_$hour2:$min2");
   #####
   # если пользователь ввел в поле дата 8 цифр, значит он имел ввиду четкую дату!
   if (length($wdate) eq "8") {
      $wdate =~ /(\d{4})(\d{2})(\d{2})/;
      $timestamp1 = timelocal($sec2,$min2,$hour2,$3,$2-1,$1-1900);
      # если ввел 0 - значит имел ввиду что запустить будильник сегодня
   } elsif ($wdate eq "0") {
      $timestamp1=$datesec;
      # если ввел 2-х значное число, значит имел ввиду что пускть задание через сколько то дней.
   } elsif (length($wdate) ge "1" and length($wdate) le "2" and $wdate ne "0") {
      $timestamp1=$datesec + $wdate*86400;
      # ну а если ерунду ввел, значит говорим ему об этом в мягкой форме и отключаемся
   } else {
      &digit_error;
   }
   # обратите внимание, я не делал проверку на правильность ввода 
   # даты, т.е. система проверяет, кол-во дней в месяце, месяцев в 
   # году, что не есть хорошо, но на мой взгляд это не критично!
   ####
   # Та же операция с временем
   if (length($wtime) eq "4") {
      $wtime =~ /(\d{2})(\d{2})/;
      $timestamp=$timestamp1-(localtime($datesec))[2]*3600-(localtime($datesec))[1]*60+($1*3600)+($2*60);
   } elsif ($wtime eq "0") {
      $timestamp=$timestamp1+60;
   } elsif (length($wtime) ge "1" and length($wtime) le "3") {
      $timestamp=$timestamp1+$wtime*60;
   } else {
      $AGI->exec('NoOp',"Не_верно_задано_время");
      &digit_error;
   }
   ####

   ($sec1,$min1,$hour1,$mday1,$mon1,$year1,$wday1,$yday1,$isdst1)=localtime($timestamp);
   $mon_sound="mon-$mon1";
   $mon1++;
   $year1=1900+$year1;

   # В результате хитрых и не очень операций, получаем время, когда 
   # необходимо позвонить нашему абоненту!
   $AGI->exec('NoOp',"Выполнение_задачи_$year1.$mon1.$mday1\_в_$hour1:$min1");

   ####
   # если же дата выполнения задачи ранее текущей даты, значит 
   # пользователь ошибся при вводе даты и времени
   if ($timestamp le $datesec) {
      $AGI->exec('NoOp',"Дата_задания_меньше_текущего_времени!");
      &digit_error;
   }
   ####
   # проделываем похожую операцию с номером куда будем звонить, при 0 
   # - звоним сами себе, если длина введенных цыфр от 3-х до 4-х, 
   # значит это локальный звонок,  если ни то не другое, пользователь 
   # имел ввиду что звоним на внешний номер!
   if ( $wnum eq "0" ) {
      $ch="SIP/$src";
      $dst="$src";
   } elsif ( length($wnum) eq "4" or length($wnum) eq "3") {
      $ch="SIP/$wnum";
      $dst="$wnum";
   } else {
      $ch="SIP/$wnum\@$trunk";
      $dst="$wnum";
   }

   #формируем имя файла для звонка
   $records="$year1$mon1$mday1\-$hour1$min1-$dst";
   $filename="/var/lib/asterisk/sounds/records/$records.sln";

   CICLE3:
   # Приятный женский голос говорит - оставьте сообщение после 
   # сигнала, затем нажмите решетку или повесьте трубку. Вешать 
   # трубку не следует, т.к. нам необходимо будет подтвердить запись, 
   # потому необходимо после надиктованного сообщения нажать решетку
   $AGI->exec('Playback',"ru11/vm-intro");
   $AGI->exec('Record',"records/$records.sln||10");

   CICLE2:
   # Тут мы озвучиваем само сообщение и куда и когда оно будет отправлено
   $AGI->exec('Playback',"ru11/vm-soobshenie");
   $AGI->exec('Playback',"ru11/na-nomer");
   $AGI->exec('SayDigits',"$dst");
   $AGI->exec('Playback',"digits/at");
   $AGI->exec('Playback',"digits/day-$wday1");
   &say_day($mday1);
   $AGI->exec('Playback',"digits/$mon_sound");
   $AGI->exec('Playback',"digits/at");
   $AGI->exec('SayNumber',"$hour1");
   $AGI->exec('Playback',"ru11/hours");
   $AGI->exec('SayNumber',"$min1");
   $AGI->exec('Playback',"ru11/minutes");
   $AGI->exec('Playback',"records/$records");

   # ввели счетчик возварата к прослушки или записи, что бы пользователь не заигрывался.
   $count++;
   if ($count eq "5") {  $AGI->exec('Playback',"ru11/goodbye");  
   unlink($filename); exit 0; }

   # подтверждаем запись (нажмите 1-н что бы принять сообщение 2-а 
   # что бы прослушать, 3-и что бы записать его заново)
   $AGI->exec('Read',"rep|ru11/vm-review|1||1|5");
   $rep = $AGI->get_variable("rep");

   if ( $rep eq "2" ){
      goto CICLE2;
   } elsif ( $rep eq "3" ) {
      unlink($filename);
      goto CICLE3;
      # если пользователь вводит 1-н, тем самым подтверждая запись, 
      # ему говорить что сообщение записано и формируется call файл в 
      # контексте wakeup экстеншен go, максимальное количество 
      # попыток дозвона 3, время ожидания на проводе 60 сек. Так же 
      # говорим что CallID у нас подменяется на NOTE (напоминание), 
      # это нужно для локальных телефонов с дисплеем, что бы можно 
      # было понять от кого звонок и передаем переменную в астериск 
      # date, говорящую когда была сделана запись, а так же 
      # переменную с именем файла, который будем слушать.
   } elsif ( $rep eq "1" ) {
      $AGI->exec('Playback',"ru11/vm-msgsaved");
      open (CALL,  "> /tmp/$records");
      print CALL  "Channel:$ch\nContext:wakeup\nExtension:go
        \nPriority:1\nMaxRetries:3\nRetryTime:60\nWaitTime:60
        \nCallerID:NOTE<$src>\nSet:date=$datesec\nSet:src=$src
        \nSet:records=$records\n";
      close (CALL);

      # меняем атрибуты файла, что бы астериск при чтении директории 
      # outgoing не запускал сразу после перемещения нашего call файла процесс дозвона.
      utime($timestamp,$timestamp,"/tmp/$records");
      move("/tmp/$records","/var/spool/asterisk/outgoing/$records");
      $AGI->exec('Playback',"ru11/goodbye");
      exit 0;
   }
   # попрощались и удалили временный файл.
   $AGI->exec('Playback',"ru11/demo-moreinfo");
   unlink($filename);
   exit 0;


Скрипт рабочий, у Вас могут возникнуть сложности только с модулями, в таком
случае идем на http://search.cpan.org/  и качаем необходимые модули.

Обратите внимание все записанные файлы хранятся в директории
/var/lib/asterisk/sounds/records в формате sln - при создании директории
поменяйте правильно права, иначе работать не будет!

Сообщение записали, теперь нужно бы его прослушать.

Итак скрипт ./reminder_listen.agi

   #!/usr/bin/perl
   use Asterisk::AGI;
   use POSIX;
   use File::Copy;
   use Time::Local;

   $AGI = new Asterisk::AGI;
   my %input = $AGI->ReadParse();
   $count=0;
   sub say_day {
      $sayday=$_[0];
      if ("$sayday" < 20) {
         $AGI->exec('Playback',"digits/h-$sayday\\n");
      } elsif ( 20 < "$sayday" and  "$sayday" < 30 ) {
         $sayday=~/\d(\d)/;
         $AGI->exec('Playback',"digits/20");
         $AGI->exec('Playback',"digits/h-$1\\n");
      } elsif ( "$sayday" == 20 ) {
         $AGI->exec('Playback',"digits/h-20n");
      } elsif ( "$sayday" == 30 ) {
         $AGI->exec('Playback',"digits/h-30n");
      } else {
         $AGI->exec('Playback',"digits/30");
         $AGI->exec('Playback',"digits/h-1\\n");
      }
   }

   # Получаем от Asterisk данные о том, откуда поступило напоминание, 
   # имя звукового файла и дата когда была сделана запись.

   $src = $AGI->get_variable('src');
   $records = $AGI->get_variable('records');
   $filename="/var/lib/asterisk/sounds/records/$records";
   $date = $AGI->get_variable('date');

   ($sec1,$min1,$hour1,$mday1,$mon1,$year1,$wday1,$yday1,$isdst1)=localtime($date);
   $mon_sound="mon-$mon1";

   CICLE1:
   # Начинаем грузить нашего абонента информацией откуда был сделан 
   # звонок, когда и что собственно от него хотели!
   $AGI->exec('Playback',"ru11/vm-from-phonenumber");
   $AGI->exec('SayDigits',"$src");

   $AGI->exec('Playback',"digits/at");
   $AGI->exec('Playback',"digits/day-$wday1");
   &say_day($mday1);
   $AGI->exec('Playback',"digits/$mon_sound");
   $AGI->exec('Playback',"digits/at");
   $AGI->exec('SayNumber',"$hour1");
   $AGI->exec('Playback',"ru11/hours");
   $AGI->exec('SayNumber',"$min1");
   $AGI->exec('Playback',"ru11/minutes");

   $AGI->exec('Playback',"records/$records");
   # В конце концов ошарашенного таким напором абонента мы 
   # переспрашиваем, хочет ли он еще раз прослушать наше сообщение, 
   # если да то у него еще в запасе 4-ре раза прослушки, далее цикл 
   # прервется и система ему скажет goodbye, удалит файл и повесит трубку.
   $AGI->exec('Read',"rep|ru11/vm-repeat|1||1|5");
   $rep = $AGI->get_variable("rep");
   if ( $rep eq "5" ){
      $count++;
      $AGI->exec('NoOp',"$count");
      if ($count eq "3") {
        $AGI->exec('Playback',"ru11/goodbye");  
        unlink($filename); exit 0; 
      }
      goto CICLE1;
   }
   unlink($filename);
   exit 0;

Вот собственно и все. Как мог подробно изложил информацию. Конечно во многих
местах код можно более изящно написать, но данная система работает довольно стабильно.
 
03.11.2010 , Автор: cr80 , Источник: http://3090607.ru/note/28-create-wa...
Ключи: asterisk, voip, voice / Лицензия: CC-BY
Раздел:    Корень / Маршрутизаторы Cisco, VoIP / VoIP

Обсуждение [ RSS ]
 
  • 1.1, Добрый Дохтур, 23:21, 04/11/2010 [ответить] [смотреть все]
  • +/
    Красиво конкорд пропиарился :)))
     
  • 1.2, BoOgatti, 13:22, 05/11/2010 [ответить] [смотреть все]
  • +/
    модная штука, начну страдать отсутствием памяти - заюзаю =)
     
  • 1.3, slay, 21:06, 05/11/2010 [ответить] [смотреть все]
  • +/
    чет я думал, что здесь про cron и outgoing на 6 строк. а тут как cisco expo - 2 дня ни о чем с перерывом на бутерброд
     
     
  • 2.4, Alexey, 17:40, 06/11/2010 [^] [ответить] [смотреть все]
  • +/
    Управление кроном по телефону с пользовательскими правами?
     
  • 1.5, анонимус, 18:00, 08/11/2010 [ответить] [смотреть все]
  • +/
    причем тут вообще cron ? oO Внимательнее надо быть
    В свое время делал подобное, но попроще на bash, но в контотре не прижилось
     
  • 1.6, Аноним, 15:06, 10/11/2010 [ответить] [смотреть все]  
  • +/
    Я урежу этот скрипт  в 2 раза!!!
     
  • 1.8, cr80, 18:29, 10/11/2010 [ответить] [смотреть все]  
  • +/
    Конечно можно урезать скрипт в разы. Perl это не мой профиль, извиняйте. Если кто напишет более изящно - пишите, опубликую сразу же.
    вторая часть моего будильника доступна в xml формате
    http://3090607.ru/note/34-wakeupii
     
  • 1.9, zoonman, 19:45, 10/11/2010 [ответить] [смотреть все]  
  • +/
    offtop: У нормальных людей напоминалка давно на мобильном...

    По существу, если использовать AGI в Perl наличие экстеншена и прочих прелестей в файле конфигурации лишено смысла. Лучше уж в БД хранить, так удобнее и интерфейс человеческий можно держать.

     
  • 1.10, cr80, 18:50, 11/11/2010 [ответить] [смотреть все]  
  • +/
    Думаю БД нужна при Web интерфейсе, при работе с будильником на телефоне база данных - дополнительная нагрузка на сервер и разработчика. тут же просто call файл создал и бросил его в директорию Asterisk и забыл о его существовании.
     

    Ваш комментарий
    Имя:         
    E-Mail:      
    Заголовок:
    Текст:



      Закладки на сайте
      Проследить за страницей
    Created 1996-2017 by Maxim Chirkov  
    ДобавитьРекламаВебмастеруГИД  
    Hosting by Ihor