Корпоративная АТС на базе Asterisk

Asterisk

Alexcr

Предпосылки

В жизни любой крупной развивающейся компании рано или поздно встаёт вопрос о расширении возможностей телефонной станции и переходе от классической телефонии к IP.

Далёкой весной 2011 и перед нашей компанией встал такой вопрос, т. к. внешние и внутренние линии требуют постоянного расширения, а количество портов на старом Panasonic KX-TA624 было задано статично и расширению не подлежало. Открытие офисов в других городах и внедрение единой службы по работе с клиентами в других городах так же подталкивало к качественным переменам.

Техническое задание

Конечный продукт должен иметь следующие характеристики:
1) иметь большое количество (в нашем случае не менее 100) внешних и внутренних линий и быть готовым к расширению;
2) уметь приветствовать пользователей в рабочее время и сообщать о том, что они позвонили в нерабочее время, когда никого нет на месте;
3) таймауты переадресации при не ответе/занятости/недоступности должны настраиваться индивидуально;
4) должны быть предусмотрены очереди. Очередь — группа номеров, распределение звонков внутри которой происходит по определенным правилам;
5) записывать лог сообщений (как текстовый, так и аудио);
6) иметь гибкую политику распределения прав на внешние звонки. Должны быть предусмотрены пользователи, которые могут звонить только на внутренние номера; на городские номера; на любые номера;
7) в зависимости от времени суток, звонить на тот или иной номер. 

Неудачный опыт

Первоначальный выбор пал на Planet ipx-1900. С характеристиками можно ознакомиться здесь:
www.planet.com.ru/en/product/product_keyf.php?id=18500

Среди прочего заявлена поддержка:
• Автоинформатор (AA) 
• Интерактивные голосовые ответы (IVR) 
• Детализированный отчет по вызовам (CDR)

На практике не работает ни один из этих параметров. Поддержка Planet'а вопросы игнорировала. Завести на это железо 8800 стандартными средствами не получалось.

Пришлось ковырять глубже. На этой PBX есть com-порт и telnet, при соединении с которым запрашивается логин и пароль. Техническая поддержка Planet'а сообщать реквизиты для входа отказалась… точнее проигнорировала просьбу. Страница с прошивкой для девайса можно найти здесь:
planet.com.ru/en/support/download2.php?id=18500&file_type=65&prod_model=IPX-1900

Прошивка оказалась не чем иным, как архивом:

$ mkdir untar && cd untar && tar xvf ../FW-IPX1900_1.16.8.dat
rootfs.jffs2
vmImage
start_install.sh
aimage.tar.gz


Самым интересным оказался start_install.sh — этот скрипт выполняется сразу после загрузки прошивки на устройство. Сразу после строки:
#!/bin/sh

добавляем строчку:
useradd -groot -proot toor


Пакуем все обратно в архив и загружаем прошивку на станцию.

Если всё пройдёт удачно, то можно будет логинится на станцию через com-порт или telnet.
Внутри был обнаружен μClinux с Asterix 1.4

И началось допиливание станции до рабочего состояния. Список изменений выкладывать не будем, потому как он, к настоящему времени, безвозвратно утерян. 

Благодаря доступу к консоли станции, удалось реализовать п.1-4 технического задания. И добавить на станции 8800, что тоже не плохо, если бы не:
1) постоянные зависания станции и внешних портов;
2) ежедневные перезагрузки станции по ночам;
3) временами, после перезагрузки, станции не удавалось синхронизироваться с временным сервером, и она сообщала клиентам, что время не рабочее, хотя на деле время было рабочим.

Тем не менее, в таком виде телефония существовала в Регтайм около полутора лет. Тем не менее руководство продолжало «пинать» техническую поддержку (да-да, именно техническую поддержку) на тему воплощения в жизнь мечты о выполненном техническом задании, и техническая поддержка разродилась.

Железо

На момент перехода с planet на обычный сервер в распоряжении имелось:
1) несколько голосовых шлюзов planet ata-150s (2 fxs порта);
2) linksys spa-3000 (1fxo + 1 fxs порт) — 2 штуки;
3) связка аналоговых телефонов (наследие Panasonic);
4) VoIP phone dlink-dph150s, dlink-dph150se;
5) сервер с ОС Debian;
6) сетевые фильтры, витая пара, свитч :)

Субъективный отзыв о железе 

VoIP dlink работают хорошо. Зависаний практически не наблюдается. Было несколько моделей с браком, но их без вопросов поменяли на новые.

Linksys — неплохо. Иногда случаются зависания, при чем зависает не сам голосовой шлюз, а порт FXO на нём. К сожалению, обнаруживается только по обращению клиентов. 

Planet ata-150s — плохо. Очень часто случаются зависания, периодически появляются шумы в трубке. Помогает только перезагрузка. Были куплены пачкой — поэтому приходится работать с тем, что есть.

Установка Asterisk

К сожалению, лог установки asterisk на debian не сохранился. Поэтому для статьи будут переведены команды установки на Ubuntu 12.04. Отличий, по большому счету, должно быть очень мало, например, в основной репозитарий debian не включен Asterisk 1.8, но он есть в backports. 

Для работы мы используем следующие пакеты:
1 asterisk — Asterisk 1.8
2 libasterisk-agi-perl — AGI модуль для perl. Об этом немного позже.
3 asterisk-mysql — расширение Asterisk, которое позволяет хранить статистику не в текстовом файле, а в базе данных
 
$ sudo aptitude install asterisk libasterisk-agi-perl asterisk-mysql mysql-server


Настройка лога звонков

1) Из-под root'а базы данных создадим новую базу и пользователя:
CREATE DATABASE astr;
GRANT ALL PRIVILEGES ON astr.* TO 'asterisk'@'localhost'
IDENTIFIED BY 'super-pass' WITH GRANT OPTION;
EXIT;


2) Заходим под только что созданным пользователем:
$ mysql -uasterisk -psuper-pass astr


3) Создаём таблицу:
CREATE TABLE `cdr` (
`calldate` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
 `clid` varchar(80) NOT NULL DEFAULT '',
 `src` varchar(80) NOT NULL DEFAULT '',
 `dst` varchar(80) NOT NULL DEFAULT '',
 `dcontext` varchar(80) NOT NULL DEFAULT '',
 `channel` varchar(80) NOT NULL DEFAULT '',
 `dstchannel` varchar(80) NOT NULL DEFAULT '',
 `lastapp` varchar(80) NOT NULL DEFAULT '',
 `lastdata` varchar(80) NOT NULL DEFAULT '',
 `duration` int(11) NOT NULL DEFAULT '0',
 `billsec` int(11) NOT NULL DEFAULT '0',
 `disposition` varchar(45) NOT NULL DEFAULT '',
 `amaflags` int(11) NOT NULL DEFAULT '0',
 `accountcode` varchar(20) NOT NULL DEFAULT '',
 `userfield` varchar(255) NOT NULL DEFAULT '',
 KEY `calldate` (`calldate`),
 KEY `dst` (`dst`),
 KEY `accountcode` (`accountcode`)
);
EXIT;


4) Переходим в каталог конфигов астериск (все дальнейшие действия необходимо выполнять от имени супер-пользователя) и делаем копию всего содержимого на всякий случай:
# cp -r ../asterisk/ ~/asterisk_config
# echo '' > cdr_mysql.conf && mcedit cdr_mysql.conf


У нас он имеет вид:
[global]
hostname=localhost
dbname=astr
table=cdr
password=super-pass
user=asterisk

[columns]
alias start => calldate


Конфиг cdr.conf остался без изменений.

Пользователи и контексты исходящих звонков

Для исходящих звонков пользователей у нас есть 4 контекста:
1 phone_int — дефолтное значение. Пользователи, которым разрешены звонки только на внутренние 3-х значные номера.
2 phone_local — пользователям можно звонить на 7-значные внутригородские номера;
3 phone_long_d — этим пользователям разрешены звонки внутри страны;
4 phone_too_long_d — международные звонки.

Контексты включены друг в друга, т.е. пользователям phone_too_long_d разрешено звонить и на внутренние номера сотрудников, и внутри города.

Приступим:

1) Разобьём sip.conf на несколько:
# mcedit sip.conf 

#include sip_general.conf
#include sip_trunk.conf
#include sip_internal.conf


sip_general.conf — основные настройки asterisk.
sip_internal.conf — внутренние номера пользователей;
sip_trunk.conf — внешние линии;

2) Пример настройки 3-х номерв в sip_internal.conf:
 
[defaults](!)
type = friend
qualify = yes
; разрешены только внутренние
; звонки, но с любого хоста
context = phone_int
canreinvite = no
host = dynamic
callgroup = 1
pickupgroup = 1

[101](defaults)
 secret = pass_for_reception
 callerid = "Sveta"<101>
 context = phone_long_d

[112](defaults)
 secret = pass_for_artur
 callerid = “Artur"<112>
 context = phone_local

[106](defaults)
 secret = pass_for_fax
 callerid = "Fax"<106>
 context = phone_too_long_d

[495](defaults)
 secret = pass_msk
 callerid = "MSK"<495>


Пользователю 101 разрешены звонки внутри России ( context = phone_long_d ); пользователю 112 ( phone_local ) можно звонить внутри города; 106 разрешено звонить куда угодно; пользователю 495 разрешены звонки только на внутренние номера (если контекст не задан, то используется контекст phone_int). Поле secret — пароль, который будет использован для авторизации.

Входящие линии

Пример основного конфига:
 
# mcedit sip_general.conf

[general]
bindport = 5060
bindaddr=0.0.0.0
allowguest = no
allowtransfer = yes
allowoverlap = no
tos_sip = cs3
tos_audio = ef
tos_video = af41
srvlookup = no
minexpiry = 900
maxexpiry = 3600
defaultexpiry = 360
checkmwi = 10
language = en
relaxdtmf = no
rtptimeout = 550
rtpholdtimeout = 600
progressinband = never
useragent = PBX
dtmfmode = rfc2833
disallow = all
domain = pbx.webnames.ru ; доменное имя станции
allow = ulaw,alaw,gsm,ilbc,g726,g729,g723 ; возможные кодеки
registertimeout = 60
registerattempts = 65535
externip = 8.8.8.8 ;внешний ip станции
externrefresh = 10
nat = yes
canreinvite = nonat
insecure = invite

register = example_num:pass_for_example_num:example_num@proxyreg_time/example_num


Настройка trunk'а:
 
# mcedit sip_trunk.conf

[trunk](!)
 type = friend
 call-limit=1
 canreinvite=no
 qualify=yes
 context= from_external
 disallow=all                    ; need to disallow=all before we can use allow=
 allow=ulaw                      ; Note: In user sections the order of codecs
 allow=alaw
 allow=g723.1                    ; Asterisk only supports g723.1 pass-thru!
 allow=g729                      ; Pass-thru only unless g729 license obtained
 allow=gsm

;;   example_num
[example_num]
 type = peer
 username = example_num
 fromuser = example_num
 secret = pass_for_example_num
 fromdomain = 1.1.1.1 ; external pbx ip
 host = 1.1.1.1 ; external pbx ip
 port = 5060
 outboundproxy = 1.1.1.1 ; external pbx ip
 outboundproxyport = 5060
 context = from_external

; наземная линия билайн
[pstn_beeline](trunk)
 username = pstn_beeline
 fromuser = pstn_beeline
 host = dynamic
 secret = pass_for_beeline

;;   88001004022 (RosTelecom)
[RTK]
 username = trace_num
 type = peer
 host = 2.2.2.2 ; external pbx ip
 insecure=port,invite
 context= from_external


Линия example_num использует авторизацию и регистрацию на удаленной станции, для совершения. Обратите внимание, для этой линии прописано:
register = example_num:pass_for_example_num:example_num@proxyreg_time/example_num


в sip_general.conf.

Линия pstn_beeline регистрации не требует — фактически эта одна из “наземных” линий, подключенных через fxo порт linksys spa 3000, но об этом немного позже.

Линия RTK — здесь не требуется ни авторизации ни регистрации. На станции нужен только пользователь и ip (поле host), с которого будут приходить звонки. Это номер 8800 — на него осуществляется только входящая телефония. В терминах Ростелекома имя пользователя — это маршрутный номер.

Для всех входящих звонков контекст один — from_external.

Dialplan (План набора) — маршрутизация звонков

Самое интересное в работе любой АТС — это план набора. План набора маршрутизирует звонки согласно правилам, которые в нём описаны. Итак:

1) Исходящие звонки.
 

# mcedit extensions.conf

[globals]

[general]
autofallthrough=yes



; звонок на трехзначный внутренний номер
; внутренние звонки обрабатываются через приложение
; dial_internal.pl - об этом чуть позже
[out_int]
; на звездочку никак не нужно реагировать
exten => *, 1, NoOp()
exten => _[1-9]XX, 1, Macro(monitor)
exten => _[1-9]XX, n, Macro(int-dial,${EXTEN})
exten => _[1-9]XX, n, Hangup()

; локальные звонки - самара
[out_local]
; включаем запись
exten => _X., 1, Macro(monitor)
exten => _[1-79]XXXXXX, 2, Dial(SIP/${EXTEN}@pstn_beeline&SIP/${EXTEN}@example_num)
exten => _0[1-79]XXXXXX, 2, Dial(SIP/${EXTEN:1}@pstn_beeline&SIP/${EXTEN:1}@example_num)
exten => _83[1-79]XXXXXX, 2, Dial(SIP/${EXTEN:2}@pstn_beeline)
exten => _85[1-79]XXXXXX, 2, Dial(SIP/${EXTEN:2}@example_num)
; исключение - звонок на наш московский номер будет осуществляться через наземную
; линию. для теста работы московской линии
exten => _094959874596, 2, Dial(SIP/${EXTEN:1}@pstn_beeline) ; msk line

; межгород
[out_long_d]
; включаем запись
exten => _X., 1, Macro(monitor)
; как будем звонить межгород
; 1) - билайн
; 2) - другая линия
exten => _08X., 2, Dial(SIP/${EXTEN:1}@pstn_beeline&SIP/${EXTEN:1}@example_num)
; позвонить на межгород через билайн или другую линию
exten => _838X., 2, Dial(SIP/${EXTEN:2}@pstn_beeline)
exten => _858X., 2, Dial(SIP/${EXTEN:2}@example_num)

; международные звонки
[out_too_long_d]
exten => _X., 1, Macro(monitor)
exten => _0X., 2, Dial(SIP/${EXTEN:1}@pstn_beeline&SIP/${EXTEN:1}@example_num)
; позвонить на межгород через билайн
exten => _83X., 2, Dial(SIP/${EXTEN:2}@pstn_beeline)
exten => _85X., 2, Dial(SIP/${EXTEN:2}@example_num)

; только внутренние номера
[phone_int]
include => out_int

; внутригородские звонки
[phone_local]
include => phone_int
include => out_local

; межгород
[phone_long_d]
include => phone_local
include => out_long_d

; международные звонки
[phone_too_long_d]
include => phone_long_d
include => out_too_long_d
; ========ИСХОДЯЩИЕ ЗВОНКИ==============



Разбор строки:
exten => _0[1-79]XXXXXX, 2, Dial(SIP/${EXTEN:1}@pstn_beeline&SIP/${EXTEN:1}@example_num)


_0[1-79]XXXXXX — маска набранного номера. Подробнее здесь ( voip.rus.net/tiki-index.php?page=Asterisk+Dialplan+Patterns )

0 — первая цифра, которая пришла
[1-79] — второй цифрой может быть любое число кроме 8, т.к. это выход на межгород
XXXXXX — любые 7 цифр

X соответствует любому числу от 0 до 9
Z соответствует любому числу от 1 до 9
N соответствует любому числу от 2 до 9

2 — приоритет, означает, что это действие будет выполнено вторым по счету.

Dial — это из самых используемых приложений asterisk. Оно предназначено для звонка на номер через определенную линию. Подробнее об этом приложении можно прочитать здесь ( voip.rus.net/tiki-index.php?page=Asterisk+cmd+Dial )

SIP/${EXTEN:1} — некое подобие regexp’ов. Например, при наборе номера 03799039 в канал будет отправлен номер 3799039.

@pstn_beeline — канал через который будет осуществлен звонок.

&SIP/${EXTEN:1}@example_num — другой канал через который будет осуществлен звонок в случае, если линия билайна занята или недоступна.
 
; внутригородские звонки
[phone_local]
include => phone_int
include => out_local


для того, чтобы получить контекст phone_local — нужно включить в него 2 контекста — phone_int и out_local; пользователю, которому подключен такой контекст (в примере выше это пользователь 112), разрешены звонки на городские номера.

2) Входящие звонки обрабатываются так:
 

; =========ВхОДЯЩИЕ ЗВОНКИ==============

[from_external]
exten => _X., 1, Macro(dial)
exten => s, 1, Macro(dial)

; =========ВхОДЯЩИЕ ЗВОНКИ==============



3) Макросы

По сути своей макросы — это функции. Макросу можно передавать параметры, которые он будет использовать в зависимости от контекста. Обратиться к макросу можно следующим образом:
exten => _[1-79]XX, n, Macro(int-dial,${EXTEN})


_[1-9]XX — номер по которому происходит дозвон.
n — приритет;
Macro — непосредственно приложение макрос.
int-dial — название макроса.
${EXTEN} — параметр, который будет передан макросу ( в данном случае номер телефона )

Подробнее о макросах читайте здесь ( voip.rus.net/tiki-index.php?page=Asterisk+cmd+Macro&highlight=Macro() ).

Макросы, которые мы используем в плане набора:
 
; =========МАКРОСЫ======================
; макрос для звонка на внутренний номер
; смотри скрипт /var/lib/asterisk/agi/dial_internal.pl
; обрабатывает логику поведения при неответе (помещение в очередь)
; таймауты по разным номерам
; информация забирается из таблицы users б.д. asterisk
[macro-int-dial]
exten => s, 1, NoOp()
exten => s, 2, AGI(/var/lib/asterisk/agi/dial_internal.pl, ${ARG1})
exten => s, n, Hangup()

; макрос включения монитора (запись разговоров)
; призван следить за личной жизнью сотрудников ООО Регтайм
; для исходящих включается в [phone_int]
; для входящих включается в /var/lib/asterisk/agi/ivr.pl
[macro-monitor]
exten => s, 1, AGI(/var/lib/asterisk/agi/monitor.pl)

; помещение звонка в очередь
[macro-groupe-dial]
exten => s, 1, Queue(${ARG1}, rtT,,,100)

; обрабатывает поступающие извне звонки
; выдаёт приветствие - передаёт ( в зависимости от времени )
; кому нужно ( секретарь<->тех.поддержка ),
; либо сообщает - “фсе давай досвидания, звони в рабочее время"
[macro-dial]
exten => s, 1, AGI(/var/lib/asterisk/agi/ivr.pl)
exten => s, n, Hangup()
; =========МАКРОСЫ======================



Почти во всех контекстах используется приложение AGI. Подробнее о приложении можно прочитать здесь ( voip.rus.net/tiki-index.php?page=Asterisk+cmd+AGI&highlight=AGI() ).

О том, как это приложение используем мы — в следующем разделе.

AGI приветствие

Задача.
Реализовать меню, которое работает следующим образом:
1) Выходные дни и нерабочее время — сообщение о том, что клиент позвонил в нерабочее время; озвучить время работы.
2) Рабочее время — приветствие; в зависимости от времени, переключить клиента на
• секретаря ( если клиент попал во время работы офиса );
• тех.поддержку в остальное рабочее время

Рабочим временем считается понедельник-пятница с 9 до 21; офис работает с 9 до 13 и с 14 до 18 по будним дням (с часу до двух в офисе обед).

Инструмент решения задачи.
Для решения задачи был выбран инструмент AGI.
AGI (Asterisk Gateway Interface) — это встроенный в Asterisk метод выполнения внешних скриптов (по аналогии с CGI для http серверов), который может расширить функциональность asterisk при помощи других языков программирования.
В качестве языка разработки выбран perl — как универсальный инструмент решения любой задачи. :-) 
На cpan есть модуль Asterisk::AGI search.cpan.org/~jamesgol/asterisk-perl-1.03/lib/Asterisk/AGI.pm.

Плюсы AGI — простота разработки; гибкость. Минусы — значительно возрастает нагрузка на сервер. Скрипт компилируется при каждом обращении, а не кэшируется; в интернетах пишут, что AGI глючит, но за время работы у нас ( ~ 0.5 года ) проблем с этим не наблюдалось. Проблем с вычислительными мощностями у нас также нет.

Решение задачи.
Все входящие звонки направляем сюда (смотрите предыдущий раздел):
exten => s, 1, AGI(/var/lib/asterisk/agi/ivr.pl) 


Обработчик входящего звонка (/var/lib/asterisk/agi/ivr.pl) будет иметь следующий вид:
use Data::Dumper; 
use warnings; 
use strict; 
use Asterisk::AGI; 
use Time::localtime; 


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

# где хранится "исключительное" время 
# например, выходной будний день 
# для check_ivr 
my $schedule_time = '/var/lib/asterisk/agi/schedule.conf'; 

# номер очереди саппорта 
my $support = '300'; 

# номер секретаря 
my $recep = '101'; 

# non_working - нерабочее время 
# working     - работает весь офис ( по умолчанию 9-13;14-18 ) 
# work_supp   - работает только служба технической поддержки (default 13-14; 18-21) 
my @ivr = (\&non_working, \&working, \&work_supp); 

#START 
$AGI->answer(); 

# определяем какой из режимов работы актуален во время звонка 
my $mode = &check_ivr(); 

# $AGI->verbose( "Mode => $mode", 0); 

&{$ivr[$mode]}; 
# завершение звонка 
$AGI->hangup(); 
exit(); 

sub non_working{ 
   # в нерабочее время просто проиграть приветствие 
   $AGI->exec('Playback', 'offduty'); 
} 

sub working{ 
   # приветствие     
   $AGI->exec('Playback', 'welcome'); 
   # в рабочее время включаем запись звонков 
   $AGI->exec('Macro', "monitor"); 
   # звоним секретарю 
   $AGI->exec('Macro', "int-dial,$recep"); 
} 

sub work_supp{ 
   $AGI->exec('Playback', 'welcome'); 
   $AGI->exec('Macro', "monitor"); 
   # звонок уходит на суппорт 
   $AGI->exec('Macro', "groupe-dial,$support"); 
} 

################# check_ivr ##################################### 
# на входе( необзятельно. по умолчанию параметры текущей даты ):# 
# $wday - номер дня недели ( формат числа - 0..6 )############### 
# $hour - час ( формат числа - 0..23 )########################### 
# $date - дата ( формат строки - число.месяц )################### 
# на выходе число( 0..2 ):####################################### 
# 0 - нерабочее время;########################################### 
# 1 - рабочий день;############################################## 
# 2 - только тех.поддержка.###################################### 
sub check_ivr { 
   my ( $wday, $hour, $date ) = @_; 
   # если не определен хотя бы один из параметров - берем текущие 
   ($wday, $hour, $date ) = &_date_now_ if !$date||!$hour||!$date; 

   my ( %check, $check); 

   #исключения - даты в которые станция работает никак обычно 
   my %sch_dates; 
   open ( SCH, $schedule_time ); 
   while ( <SCH> ) { 
       if ( $_ =~ /^(\d{1,2}\.\d{1,2})\s*(.*)[\r\n]*$/) { 
           my ( $sch_date, $sch ) = ($1, $2); 
           $date =~ s/^0(\d)/$1\./; $date =~ s/0(\d)$/$1/; 
           $sch ||= '0'; 
           $sch_dates{$sch_date} = $sch; 
       } 
   } 
   close(SCH); 

   foreach my $schedule_d (keys %sch_dates){ 
       if ( $date eq $schedule_d ) { 
           if ( $sch_dates{$date} ) { 
               return &_check_time_( $hour, $sch_dates{$date} ); 
           } 
           else { return $sch_dates{$date} } 
       } 
   } 

   # выходной ли день? принимает значение 1( рабочий ) и 0 ( выходной ) 
   $check{wday} = $wday>0&&$wday<6 ? '1':'0'; 
   # принимает значения 0, 1, 2 - смотри _check_time_ 
   $check{hour} = &_check_time_( $hour ); 

   $check = 1; 
   foreach ( values %check ) { $check *= $_; } 
   return $check; 

   sub _check_time_ { 
       # $hour_tm - текущее время 
       # $wr_time - work time ( время работы офиса ) строка вида [9,13,18,21] 
       my ( $hour_tm, $wr_time) = @_; 

       # $wr_time - work time ( время работы офиса ) 
       # преобразованная строка (хэш) 
       my %wr_time = &_parse_work_time_( $wr_time ); 

       # 0 - не рабочее время 
       # 1 - рабочее время офиса ( 9-13; 14-18 ) 
       # 2 - c 13 до 14; с 18 до 21 
       my $check_tm = 0; 
       if( $hour_tm>$wr_time{st_office}&&$hour_tm<$wr_time{end_support} ) { 
           if ( $hour_tm==$wr_time{lunch}||$hour_tm>$wr_time{end_office} ) { 
               $check_tm = 2; 
           } 
           else { $check_tm = 1 } 
       } 
       return $check_tm; 
   } 

   # делает из строки вида [9,13,18,21] 
   # хэш массив вида 
   # %work_time = ( st_office   => 8, 
   #                lunch       => 13, 
   #                end_office  => 17, 
   #                end_support => 21); 
   sub _parse_work_time_ { 
       my ( $work_time ) = @_; 
       # если не определено либо равно 1 
       # дефолтное значение фактически. 
       $work_time = "[9,13,18,21]" if (!$work_time||$work_time eq 1 ); 
       $work_time =~ s/^\[(.+)\][\r\n]*$/$1/; 
       my %work_time; 
       ( $work_time{st_office}, $work_time{lunch}, $work_time{end_office}, $work_time{end_support}, $work_time{lunch_support} ) = split /\s*,\s*/, $work_time; 
       $work_time{st_office} -= 1; 
       $work_time{end_office} -= 1; 
       return %work_time; 
   } 


   sub _date_now_ { 
       return ( localtime->wday, 
                localtime->hour, 
                localtime->mday.'.'.(localtime->mon+1) ); 
   } 

} 

__END__



Файл /var/lib/asterisk/agi/schedule.conf хранит в себе «исключительное» время, т.е. время когда офис работает не как обычно, например, праздничный или сокращенный день. Пример:

# 1 — дата (день.месяц)
# 2 — какой день (параметр необязателе)
# 0-нерабочий либо пустой
# 1- обычный рабочий ( [9,13,18,21] ) либо массив
# 2.1 — время начала работы офиса (9)
# 2.2 — обед ( 13, т.е. с 13 до 14 )
# 2.3 — время окончания работы офиса (18)
# 2.4 — время окончания работы тех.поддержки
# дата вводится без нулей. 06.11 — не работает; 6.11 — работает
29.12 [9,11,17,20]
31.12

29.12 [9,11,17,20] ( 29 декабря ) — сокращенный день. Офис работает с 9 до 17; обед с 11 до 12; техническая поддержка работает до 20. 
31.12 — нерабочий день.

AGI обработчик для звонков

Задача.
При звонке на номер звонок должен быть переадресован через определенный таймаут/занятости/не ответу/недоступности на другой номер (как правило, это одна из очередей). «Таймаут» и «другой номер» — переменные динамичные — для каждого номера они могут быть заданы индивидуально.

Инструмент.
Как и в прошлом разделе — AGI.

Решение задачи.

Внутренние номера сотрудников хранит база данных mysql в таблице users. 
Создадим таблицу:
CREATE TABLE `users` (
 `num` int(11) PRIMARY KEY NOT NULL,
 `timeout` tinyint(4) NOT NULL DEFAULT '10',
 `queue` int(11) DEFAULT NULL );


num — номер телефона сотрудника;
timeout — время в секундах, через которое звонок будет переадресован;
queue — номер очереди, куда будет переадресован звонок.

добавим записи для номеров:
INSERT INTO `users` VALUES (101,10,300), (106,20,0), (112,20,0);


проверим, что получилось:
SELECT * FROM users;
+-----+---------+-------+
| num | timeout | queue |
+-----+---------+-------+
| 101 | 10 | 300 |
| 106 | 20 | 0 |
| 112 | 20 | 0 |
+-----+---------+-------+

При звонке на 101 через 10 секунд звонок уйдет на 300 номер (у нас это номер технической поддержки); при звонке на 112 или 106 звонок никуда не уйдёт, а просто завершится через 20 секунд.

Обратиться к обработчику можно следующим образом( AGI(/var/lib/asterisk/agi/dial_internal.pl, ${ARG1}) ):

В dialplanе это:
exten => _[1-9]XX, n, Macro(int-dial,${EXTEN})

[macro-int-dial]
exten => s, 1, NoOp()
exten => s, 2, AGI(/var/lib/asterisk/agi/dial_internal.pl, ${ARG1})
exten => s, n, Hangup()


Номер телефона попадает в макрос переменной ${EXTEN}, а потом передаётся обработчику /var/lib/asterisk/agi/dial_internal.pl с помощью ${ARG1}.

Код обработчика:
 
#!/usr/bin/perl
use Data::Dumper;
use warnings;
use strict;
use Asterisk::AGI;
use DBI;

my $user = 'asterisk';
my $pass = 'super-pass';
my $db = 'astr';

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

$AGI->answer();

# на какой номер звонок
my $exten = $input{arg_1};

# берём таймаут и очередь, к которой привязан звонок из базы
my ($timeout, $queue) = &get_timeout($exten);
# дефолтный таймаут при звонке
# на случай, если в базе нет.
$timeout ||= '10';
#$AGI->verbose("$timeout $queue", 0);
$AGI->exec('Dial', "SIP/$exten, $timeout, Tt");
# если для номера по занятости либо не ответу определены очереди
if ( $queue ) {
#    $AGI->verbose('$queue', 0);
   $AGI->exec('Macro', "groupe-dial,$queue") if ( $AGI->get_variable('DIALSTATUS') ne 'ANSWER');
}
$AGI->hangup();
exit();

sub get_timeout {
   my ($num) = @_;
   my $dbh = DBI->connect("DBI:mysql:database=$db;host=localhost",
                           $user, $pass,
                           {'RaiseError' => 1});

   my $sth = $dbh->prepare("SELECT * FROM users WHERE num like $num");
   $sth->execute();
   my $ref = $sth->fetchrow_hashref();
   ( $timeout, $queue ) = ( ${$ref}{timeout}, ${$ref}{queue});
   $sth->finish ();
   $dbh->disconnect();
   return ($timeout, $queue);
}

__END__


Аудиологи звонков

Все звонки записываются исключительно в рабочее время. В записи звонков так же используется AGI. Можно было бы обойтись и без него, но гораздо удобнее, когда логи раскладываются в отдельные директории. 
Пример файла лога: ./2013/03/28/20130328.150550.9033779401.wav,
т.е. ./год/месяц/день/год_месяц_число.час_минут_секунда.номер_с_которого_осущ._звонок.wav

имя файла спроектировано таким образом, чтобы можно было бы сложить все файлы в одну директорию и grep'нуть по определенному параметру (номеру телефона/времени и т.п.)

Код обработчика /var/lib/asterisk/agi/monitor.pl:
#!/usr/bin/perl
use Data::Dumper;
use warnings;
use strict;
use Asterisk::AGI;
use File::Path qw(make_path);

# директория с аудиологами
my $dir = '/mnt/pbx/';

my $AGI = new Asterisk::AGI;
my %input = $AGI->ReadParse();
# свойство - если определено, добавляется в конец
# имени файла 
my $prop = $input{arg_1};

$AGI->answer();

my ($date, $time) = split / /, $AGI->get_variable('CDR(start)');

$time =~ s/://g; $date =~ s/-/\//g; $date .= '/'; 
my $dir = $dir.$date;
# для имени файла уберем разделители в дате
$date =~ s/\///g;
# если свойство не определено пишем откуда звонок
# на момент написание свойство могло быть только - 'ivr'
$prop ||= $AGI->get_variable('CDR(src)');
# уберем лишние символы
$prop =~ s/[\+\.\'\"\:\(\)\[\]\&\^\$\#\@\!\%\*\s]//g;
my $file = $prop ? $dir.$date.".$time.$prop" : $dir.$date.".$time.anon";
# расширение файла
$file .= '.wav';

$AGI->exec('MixMonitor', "$file, a");
exit();
__END__


Подробнее о приложении MixMonitor можно прочитать здесь ( www.voip-info.org/wiki/view/MixMonitor )

Очереди

С очередями всё относительно просто. Конфиг называется queues.conf. Очереди у нас 2 — техническая поддержка (номер 300) и бухгалтерия (301):
 
[general]
persistentmembers=yes
autofill=yes
autopause=no
monitor-type=MixMonitor
strategy=ringall ; звонить всем.
ringinuse=no
timeout=100
retry=2
wrapuptime=0
maxlen=0
defaultrule = plus10

[301]
member => SIP/105,1
member => SIP/109,1

[300]
member => SIP/102,1
member => SIP/108,1


Подробнее о возможностях очередей можно прочитать здесь:
www.voip-info.org/wiki/view/Asterisk+config+queues.conf

Настройка внешних линий через linksys spa-3000

Задача.
В linksys spa-3000 есть один fxo-порт и один fxs. При звонке по номеру, подключенному к голосовому шлюзу ( далее г.ш. ), звонок нужно направить на fxs порт (там у нас стоит факс). При этом у нас должна быть возможность
1) совершать звонки через линию, подключенную к fxo порту из офиса;
2) переключать на номер факса, если звонок пришёл через другую линию;
3) звонить с факса через любую линию, подключенную к станции.

Решение.
Для того, чтобы осуществить задуманное, нужно настроить г.ш. следующим образом. Переходим на страницу Admin login->Advanced->PSTN Line:
Proxy: 10.0.0.5 # (ip станции)
Outbound Proxy: 10.0.0.5 # (ip станции)
Register: yes
User ID: pstn_beeline
Password: pass_for_beeline 

# для того, чтобы звонок поступал в обход станции сразу на факс.
Dial Plan 2: (<:@gw0>)
PSTN Caller Default DP:2

Настройка внешней линии на станции описана в разделе внешние линии (pstn_beeline).

106 номер fax:
Proxy: 10.0.0.5 # (ip станции)
Outbound Proxy: 10.0.0.5 # (ip станции)
Register: yes
User ID: 106
Password: pass_for_fax

настройка внутреннего номера на станции описана в разделе пользователи и контексты исходящих звонков (106)

Полезные команды

asterisk -r — доступ к командной строке asterisk
asterisk -rx 'sip show peers' — вывести в консоль список подключенных пользователей без входа в консоль asterisk. С ключом -x можно выполнять любую команду, не заходя в консоль asterisk

Команды, консоли asterisk, которые у нас используются чаще всего:
sip reload — перезагрузить информацию из конфига sip.conf
dialplan reload — перезагрузить план набора
sip show channels — список созданных каналов
sip set debug ip ip_адрес_peer'a — включить debug конкретного канала. Показывает пакеты которые ходят между нашей станцией и удаленной станцией. Незаменимо для отладки
core set verbose 3 — включить режим отладки. Показывает какие приложения выполняются во время создания канала.