Elastix — Asterisk как система экстренного голосового оповещения

  • Tutorial

Требуется сделать систему экстренного голосового оповещения в случае аварий и прочих происшествий. Схема работы следующая:
Ответственное лицо звонит на определенный номер телефона, набирает код и попадает в голосовое меню системы оповещения. Там ему предлагается записать сообщение, прослушать его, сохранить и система должна совершать звонки одновременно используя 5 телефонных линий на заранее заданные в файле номера телефонов. Причем в случае, если не взяли трубку — перезванивать оповещаемому. По окончании оповещения на указанную почту должен приходить файл с номерами телефонов на которые не смогли дозвониться вообще.

Если интересно как это работает прошу под кат.

Для совершения звонков в Астериске есть call файлы. При помещении файла в директорию /var/spool/asterisk/outgoing он автоматически совершает звонок. Давайте для начала разберемся из чего состоят call файлы и что у них внутри.

Внутри каждого файла может быть несколько переменных:
Channel: - Указывает канал для исходящего вызова
CallerID: Name - Соответственно имя, от кого будет исходить вызов
MaxRetries: - Вот как раз очень нужный параметр количества попыток дозвона, если установить 0, то будет считаться что это 1 попытка.
RetryTime: - Время между попытками неудачного вызова, задается в секундах, противоречивый на практике параметр, если поставить мало - то вероятность дозвониться падает, а если много - то сильно может увеличить общее время обзвона (при большом количестве номеров). Приходится выбирать золотую середину.
WaitTime: - Время в секундах сколько будет звонить телефон у каждого абонента из списка телефонов. Думаю в районе 60 секунд вполне удобно, больше смысла нету, а меньше в учреждениях могут не успеть дойти до телефона.
Account:
- Должно использоваться для установки поля “account code”для записи в CDR, но я не использовал
Context: <context-name> - Контекст который будет использоваться для совершения дальнейших действий(когда прошел дозвон)
Extension: - Название Extension от которого будет совершаться звонок.
Priority: - Номер приоритета для Extension, с которого нужно начать выполнение.
Set:
- Установка переменных канала для использования их в логике обработки вызова на заданный екстеншен.
Application: - Имя приложения Asterisk, которое необходимо выполнить. Если используется приложение - то не будет использованы context, extension и priority.
Data: - Параметры для запускаемого приложения. Тоже не использовал.
Archive: Yes/No – Переносить или нет .call файл в поддиректорию "outgoing_done" с установленным значением поля "Status: значение", где значение может быть: Completed, Expired или Failed. Тоже не использовал.

Основные поля в файле посмотрели, теперь я приведу как выглядит файл у меня:

Channel: Local/11%1%@from-internal/n
CallerID: <5102>
MaxRetries: 4
RetryTime: 60
WaitTime: 60
Context: startmessage
Extension: 5102
Priority: 1


Теперь по порядку. В переменной канала используется Local для того чтобы все эти звонки присутствовали в CDR логах(не забываем про необходимость анализировать обзвон - для формирования списка недоступных телефонов). Имя звонящего, потом установлено 5 попыток дозвона через каждые 60 секунд, и звонить номер будет тоже 60 секунд. После того как Астериск дозвонится абоненту - будет использоваться самописный контекст startmessage. Звонить будет extension 5102, приоритет 1.

Перед номером телефона который будет вставлен в файл вставляется 11, это сделано для удобства маршрутизации звонка через нужный транк. То есть в FreePBX создается Outbound route в котором из номера вырезается вначале 11 и ставим выход через нужный транк. Парсер который в конце обзвона формирует список файлов "кому не дозвонились" тоже вырезает эти две единицы.

Раз уж мы коснулись контекстов можно заглянуть в /etc/asterisk/extensions_custom.conf (стоит Elastix)

Вначале стоит строчка exten => 9876,1,Goto(testcontext,s,1) , которая закидывает при звонке на 9876 на наш контекст, в конце статьи опишу для чего.
Далее у меня прописано 2 контекста. Первый отвечает за запись сообщения, и начало обзвона. Второй за действия, которые будет совершать система, после дозвона абоненту.

Первый контекст :
[testcontext]
exten => s,1,Answer
exten => s,n,Wait(2)
exten => s,n,Playback(/var/lib/asterisk/sounds/custom/privet-zapis)
exten => s,n,Record(obzvon-message.wav)
exten => s,n,Playback(/var/lib/asterisk/sounds/obzvon-message)
exten => s,n,Playback(/var/lib/asterisk/sounds/custom/zapusk)
exten => s,n,WaitExten(10)
exten => s,n,Hangup()
exten => 999,1,System(echo "" > /var/log/asterisk/cdr-custom/Simple.csv)
exten => 999,n,System(/mnt/script/parser.bash /mnt/CallCenter/telefony.txt /mnt/script/main.call)
exten => 999,n,System(/mnt/script/startcall.bash)
exten => 0,1, Hangup()

Описываю построчно - снимаем трубку, ждём 2 секунды, воспроизводим приветствие из файла /var/lib/asterisk/sounds/custom/privet-zapis , записываем сообщение obzvon-message.wav, сразу после записи мы воспроизводим его в трубку и запускаем звуковой файл с дальнейшими инструкциями /var/lib/asterisk/sounds/custom/zapusk. На данном моменте предлагается ввести пароль для запуска системы оповещения и ждём 10 скунд для ввода пароля. Если будет набрано 999, то запускаем скрипты запуска, если нет - вешаем трубку.

После того как набрали 999, первым делом Астериск чистит содержимое файла с кастомным логом /var/log/asterisk/cdr-custom/Simple.csv (Позже рассмотрим как он сделан), потом запускает скрипт /mnt/script/parser.bash с двумя аргументами, первый это /mnt/CallCenter/telefony.txt - путь к файлу телефонов(по одному телефону на строку в нужном формате), второй это /mnt/script/main.call - файл с шаблоном call файла. Шаблон call файла приведен выше. Далее запускается скрипт /mnt/script/startcall.bash.

Хочу отметить что блокнотом windows лучше этот файл с телефонами не редактирова, т.к. он вставляет символы из за которых нарушается работоспособность, я пользовался Notepad ++, когда делал это из под Windows.

Приведу свой файл для формирования CDR - /etc/asterisk/cdr_custom.conf:

[mappings]
Simple.csv => ${CSV_QUOTE(${CDR(clid)})},${CSV_QUOTE(${CDR(src)})},${CSV_QUOTE(${CDR(dst)})},${CSV_QUOTE(${CDR(dcontext)})},${CSV_QUOTE(${CDR(channel)})},$$


Теперь разберемся что находится в скриптах.
Вот содержимое файла /mnt/script/parser.bash :
#!/bin/bash
#первый аргумент - файл с телефонами, второй файл шаблона
FILE=`cat $1 | sort -u`

for I in $FILE
do
if [ -n "$I" ]
then
sed -e 's/\%1\%/'$I'/g' < $2 >> /mnt/script/tmp/$I.call
fi
done;


Из файла с телефонами вырезается первая строка и вставляется в call файл вместо %1%, создающийся в папке /mnt/script/tmp/ с именем "номер телефона".call .

Содержимое скрипта /mnt/script/startcall.bash :
#!/bin/bash
# Папка в которой лежат свеже-сформированные call файлы
OutDir="/mnt/script/tmp/"
# Папка в которую будут помещаться call файлы
inDir="/var/spool/asterisk/outgoing/"
# Количество call файлов одновременно находящихся в папке (равняется количеству используемых для звонка линий)
CountIn=5;

movefile ()
# Считаем количество файлов в inDir
{
CounFileInDir=$(find $inDir -name "*.call" | wc -l);
# Если количество файлов в inDir - меньше значения CountIn, то перемещаем файл в эту директорию.
if (("$CounFileInDir" < "$CountIn")); then
mv $I $inDir
# Иначе ждём 30 секунд и повторяем функцию заново.
else
sleep 30;
movefile;
fi
}
# В переменную помещаются имена всех файлов *.call в директории OutDir.
FILE=`find $OutDir -name "*.call"`
# Пробегаем по всем значениям в переменной FILE, если файл существует, то выполняем movefile.
for I in $FILE
do
if [ -n "$I" ]
then
movefile;
fi
done;
# По окончании работы скрипта - запускаем следующий скрипт.
/mnt/script/result.bash


Содержимое скрипта /mnt/script/result.bash :
#!/bin/bash
# Файл со списком телефонов
telefoni=/mnt/CallCenter/telefony.txt
# Лог файл из которого будем парсить.
logfile=/var/log/asterisk/cdr-custom/Simple.csv;

# Создаём временный файл.
cp $telefoni telefoni.tmp
# В переменную FILE заносим отсортированный временный файл только с уникальными значениями.
FILE=`cat telefoni.tmp | sort -u | uniq`
# Пробегаем по всем значениям переменной FILE , если номер телефона существует - то в переменной logfile ищутся все значения содержащие ANSWERED.
# startmessage, ищется значение текущего номера телефона, если все значения в строке сошлись то из строки берется номер 
#телефона, в котором спереди вырезаются 11 (использующиеся для маршрутизации вызова) и всё что находится после @ сортируется по уникальным записям и передается во временный файл temp

for I in $FILE
do
if [ -n "$I" ]
then
rm temp.temp
cat $logfile | grep "ANSWERED" | grep "startmessage" | grep $I | awk 'BEGIN{FS=","}{print $5}' | sed -e 's/.*\/11//g' -e 's/\@.*//g' | uniq >> temp$
fi
done;

# Переменная FILE1 получает значение файла temp.temp, сортирует и оставляет только уникальные записи.
FILE1=`cat temp.temp | sort -u | uniq`

# Формируем список кому не дозвонились.
for J in $FILE1
do
if [ -n "$J" ]
then
cat telefoni.tmp | grep -v $J > telefoni1.tmp
mv telefoni1.tmp telefoni.tmp
fi
done;
rm temp.temp; 

Отправляем список телефонов кому не дозвонились на почтовый ящик
cat telefoni.tmp | mail -s Ne_Dozvon testmail@example.com


Осталась самая малость - сделать входящую маршрутизацию для звонка. Заходим в Inbound routes и создаем маршрут с CID номером ответственного за запуск системы человека. Тоесть когда он позвонит на любой номер - то попадет на систему обзвона, если не он, то пойдёт по стандартному маршруту. Можно также задать DID чтобы прикрепить к какому то конкретному номеру телефона фирмы. Теперь в destination надо указать Misc Destinations который мы сейчас создадим. Заходим в Misc Destinations, создаём новый и вписываем 9876 в строку dial. (Помните мы добавляли строчку в extensions_custom.conf? )

Вот собственно мы и добились нужного результата, вариантов использования конечно масса, переделывать тоже можно как угодно, но эту статью я решил сделать в виде заметки для себя же, чтобы не забыть что и как работает. То что многое криво и возможно не совсем правильно я знаю, посему конструктивную критику только поддерживаю.

Средняя зарплата в IT

113 000 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 5 503 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 12

    –4
    Комментов пачка.
    Интересная, наверное, статья.
      0
      в отличии от вашего комментария статья имеет право на существование и интересна как основа для развития дальнейших идей
      0
      Автору спасибо, пригодится.
        0
        мы написали свой велосипед — только очередь формируется в базе.
          0
          Недавно делал нечто отдалённо похожее на Ruby и Freeswitch. Честно говоря, не думал, что с Asterisk'ом всё настолько сложнее.
            0
            Может я что-то не понимаю, но зачем городить такой огород?
            Достаточно добавить разрешение в manager и написать скрипт за минуту:

            #!/bin/bash

            AMI_HOST=127.0.0.1
            AMI_PORT=5038
            AMI_USER=test
            AMI_PASS=test

            CALLERID='«Petr Pyatochkin» '
            CALL_CONTEXT=from-local-dialer
            TARGET_CONTEXT=conf

            FAILED_LIST=failed.txt

            :> ${FAILED_LIST}

            for num in $*; do
            res_str=$(cat ${FAILED_LIST}
            fi

            done

            echo «cat ${FAILED_LIST} | mail -s Ne_Dozvon testmail@example.com»

            rm -f ${FAILED_LIST}

            UPD: хабр съел код, захостил тут: pastebin.com/hQ4L338S
              0
              Вы не учитываете что нужно контролировать количество одновременно занятых линий. Количество call файлов в папке Outgoing находящихся одновременно равняется количеству одновременно совершаемых звонков и соответственно занятых линий. Причем если вы закинете туда больше файлов, чем у Вас есть реальных линий, то соответственно не сможете дозвониться и очень быстро пройдут 5 попыток дозвона — данный звонок пойдёт в список не дозвонившихся и можно начинать сначала.
                0
                Да, у Вас немного другие задачи.
                Впрочем, вычисление кол-ва активных линий решается проверкой в цикле с помошью Originate: command.

                'core show channels count', но вероятно, Вам не нужно считать Local каналы, т.к. не всегда они могут равняться количеству SIP звонков.
                Лучше всего будет например так:
                # asterisk -rx «core show channels» | grep ^SIP | wc -l
                4

                  0
                  Думаю это просто вопрос религии =)) С какой стороны отлавливать линии. Хотя мне кажется что в таком случае тоже может не сработать. Представьте себе сервер на котором висит порядка 50 абонентов которые куда то звонят, с отдельного транка вы запускаете оповещение, как будете считать линии? А если транков 20 и всё это связано с кучей астерисков которые гоняют друг через друга звонки? Мне кажется лучше контролировать количество файлов в папке.
                    0
                    Религия? Ок :-)))
                    Только у меня в продакшне кол-во линий считается через AMI, а не через call-файлы. Всегда онлайн 150-200 линий. Никаких проблем нет.
                    К тому же, + AMI Events. В этом случае работает полная асинхронная модель и все учитывается полностью.
                      0
                      Через AMI чесно говоря пока не умею, было бы интересно полностью посмотреть на схему работы.
              0
              Я описывал упрощённый вариант в этой статье

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое