В нашей организации используется Астериск, а для исходящих вызовов к нему подключен GSM-шлюз Yeastar TG800 на 8 симок. Каждый месяц мы покупаем пакет минут на каждую симку.

Для совершения исходящего вызова обычно используется перебор симок по порядку, пока не найдется свободная:

exten => _X.,n,Dial(SIP/gsm1/${EXTEN},,tT)
exten => _X.,n,Dial(SIP/gsm2/${EXTEN},,tT)
exten => _X.,n,Dial(SIP/gsm3/${EXTEN},,tT)

и т. д.

В этом случае больше всего вызовов проходит через первую симку, а меньше всего через последнюю. Поэтому к середине месяца минуты на первых симках заканчиваются раньше, чем на последних, и возникает ситуация, когда позвонить можно допустим через 4 симки из 8, так как на первых четырех минуты закончились. Это плохо, так как остаются 4 исходящие линии вместо 8.

Чтобы этого избежать, нужно делать вызовы не в одном и том же порядке, а в соответствии с остатком минут на симках.

Попутно я записываю остатки минут, актуальность (во сколько были проверены остатки) и когда пакет был активирован в БД MySQL.

Можно зайти на страничку и быстро узнать остатки:

image

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

Будем проверять остатки каждый час, для этого в крон добавим вызов скрипта (у меня каждую 53-ю минуту часа):

53 */1  * * *   root    php %path-to-script%/getbalance.php

Далее нам нужно создать 8 текстовых файлов (или сколько у вас используется симок) с именами от nc2.txt до nc9.txt. Номера такие, потому что шлюз нумерует свои порты именно так, от 2 до 9.

В каждом файле команды шлюзу. Пример для nc2.txt:

action: login
username: ******
secret: *******

action: smscommand
command: gsm send ussd 2 *255*0#

action: logoff

Пример приведен для Теле2. Соответственно в файле nc3.txt вместо «ussd 2» пишем «ussd 3». Скрипт передает эти команды шлюзу, в ответ мы получаем файлы с именами от nc2out.txt до nc9out.txt.

Ответ будет такой (для Теле2):

Asterisk Call Manager/1.1
Response: Success
Message: Authentication accepted

Response: Follows
Privilege: SMSCommand
1:Received USSD success on span: 2
        USSD Responses: 2
        USSD Code: 64
        USSD Len: 85
        USSD Message: Ostatok v pakete: 968 min./mes., 5000 SMS/mes., 20480 MB/mes. Parametry' tarifa *107#
--END COMMAND--

Response: Goodbye
Message: Thanks for all the fish.

Уточнение: нужно переключить ответы оператора на транслит, иначе получаем текст в неудобной кодировке.

Дальше мы выдергиваем количество оставшихся минут, сортируем и записываем порядок вызова в файлы от out1.txt до out8.txt (это уже для астериска, поэтому нумерация нормальная). То есть если порядок вызова оказался 5, 4, 3, 2, 1, 7, 8, 6, то в файл out1.txt мы пишем цифру 5, в файл out2.txt пишем 4 и т. д.

Осталось в диалплане прочитать эти файлы и делать вызовы в правильном порядке:

exten => _X.,n,set(SIMNUM1=${SHELL(/usr/bin/php -f %pathtoscript%/getsim1.php)})
exten => _X.,n,set(SIMNUM2=${SHELL(/usr/bin/php -f %pathtoscript%/getsim2.php)})
exten => _X.,n,set(SIMNUM3=${SHELL(/usr/bin/php -f %pathtoscript%/getsim3.php)})
exten => _X.,n,set(SIMNUM4=${SHELL(/usr/bin/php -f %pathtoscript%/getsim4.php)})
exten => _X.,n,set(SIMNUM5=${SHELL(/usr/bin/php -f %pathtoscript%/getsim5.php)})
exten => _X.,n,set(SIMNUM6=${SHELL(/usr/bin/php -f %pathtoscript%/getsim6.php)})
exten => _X.,n,set(SIMNUM7=${SHELL(/usr/bin/php -f %pathtoscript%/getsim7.php)})
exten => _X.,n,set(SIMNUM8=${SHELL(/usr/bin/php -f %pathtoscript%/getsim8.php)})

exten => _X.,n,Dial(SIP/gsm${SIMNUM1}/${EXTEN},,tT)
exten => _X.,n,Dial(SIP/gsm${SIMNUM2}/${EXTEN},,tT)
exten => _X.,n,Dial(SIP/gsm${SIMNUM3}/${EXTEN},,tT)
exten => _X.,n,Dial(SIP/gsm${SIMNUM4}/${EXTEN},,tT)
exten => _X.,n,Dial(SIP/gsm${SIMNUM5}/${EXTEN},,tT)
exten => _X.,n,Dial(SIP/gsm${SIMNUM6}/${EXTEN},,tT)
exten => _X.,n,Dial(SIP/gsm${SIMNUM7}/${EXTEN},,tT)
exten => _X.,n,Dial(SIP/gsm${SIMNUM8}/${EXTEN},,tT)

Да, чуть не забыл, файл getsim1.php:

<?php
$t = file_get_contents('%pathtoscript%/out1.txt');
echo $t[0];
return $t[0];
?>

Теперь собственно сам главный скрипт getbalance.php.

IP шлюза возьмем 10.10.1.1.

Знаю, что нужно сделать цикл вместо копипасты, но руководствуюсь золотым правилом админа: работает — не трогай :-)

<?php
include '/var/lib/asterisk/agi-bin/lib.php';

$s=0;
a2:
$s++;
if ($s>10) exit;
system('nc 10.10.1.1 5038 < %pathtoscript%/nc2.txt > %pathtoscript%/nc2out.txt');
$text = file_get_contents('%pathtoscript%/nc2out.txt');
if(preg_match('/te:\ (.*)\ min/',$text,$matches)) $text2 = $matches[1]; else goto a2;

$s=0;
a3:
$s++;
if ($s>10) exit;
system('nc 10.10.1.1 5038 < %pathtoscript%/nc3.txt > %pathtoscript%/nc3out.txt');
$text = file_get_contents('%pathtoscript%/nc3out.txt');
if(preg_match('/te:\ (.*)\ min/',$text,$matches)) $text3 = $matches[1]; else goto a3;

$s=0;
a4:
$s++;
if ($s>10) exit;
system('nc 10.10.1.1 5038 < %pathtoscript%/nc4.txt > %pathtoscript%/nc4out.txt');
$text = file_get_contents('%pathtoscript%/nc4out.txt');
if(preg_match('/te:\ (.*)\ min/',$text,$matches)) $text4 = $matches[1]; else goto a4;

$s=0;
a5:
$s++;
if ($s>10) exit;
system('nc 10.10.1.1 5038 < %pathtoscript%/nc5.txt > %pathtoscript%/nc5out.txt');
$text = file_get_contents('%pathtoscript%/nc5out.txt');
if(preg_match('/te:\ (.*)\ min/',$text,$matches)) $text5 = $matches[1]; else goto a5;

$s=0;
a6:
$s++;
if ($s>10) exit;
system('nc 10.10.1.1 5038 < %pathtoscript%/nc6.txt > %pathtoscript%/nc6out.txt');
$text = file_get_contents('%pathtoscript%/nc6out.txt');
if(preg_match('/te:\ (.*)\ min/',$text,$matches)) $text6 = $matches[1]; else goto a6;

$s=0;
a7:
$s++;
if ($s>10) exit;
system('nc 10.10.1.1 5038 < %pathtoscript%/nc7.txt > %pathtoscript%/nc7out.txt');
$text = file_get_contents('%pathtoscript%/nc7out.txt');
if(preg_match('/te:\ (.*)\ min/',$text,$matches)) $text7 = $matches[1]; else goto a7;

$s=0;
a8:
$s++;
if ($s>10) exit;
system('nc 10.10.1.1 5038 < %pathtoscript%/nc8.txt > %pathtoscript%/nc8out.txt');
$text = file_get_contents('%pathtoscript%/nc8out.txt');
if(preg_match('/te:\ (.*)\ min/',$text,$matches)) $text8 = $matches[1]; else goto a8;

$s=0;
a9:
$s++;
if ($s>10) exit;
system('nc 10.10.1.1 5038 < %pathtoscript%/nc9.txt > %pathtoscript%/nc9out.txt');
$text = file_get_contents('%pathtoscript%/nc9out.txt');
if(preg_match('/te:\ (.*)\ min/',$text,$matches)) $text9 = $matches[1]; else goto a9;

$text10 = "0";

system('echo "'.date('l dS F Y h:i:s A').' '.$text2.' '.$text3.' '.$text4.' '.$text5.' '.$text6.' '.$text7.' '.$text8.' '.$text9.'" >>%pathtoscript%/balance.txt');

$row = mysql_query("select minutes from `lines` where clid like '%2636'",$link);
$row1 = mysql_fetch_assoc($row);
$old1 = $row1['minutes']+0;
$new1 = $text2+0;

if ($old1 < $new1) $row = mysql_query("update `lines` set balancedate=NOW()",$link);

$row = mysql_query("update `lines` set balance=".$text10.", minutesdate=NOW(), minutes=".$text2." where clid like '%2636'",$link);
$row = mysql_query("update `lines` set balance=".$text10.", minutesdate=NOW(), minutes=".$text3." where clid like '%4036'",$link);
$row = mysql_query("update `lines` set balance=".$text10.", minutesdate=NOW(), minutes=".$text4." where clid like '%4977'",$link);
$row = mysql_query("update `lines` set balance=".$text10.", minutesdate=NOW(), minutes=".$text5." where clid like '%2414'",$link);
$row = mysql_query("update `lines` set balance=".$text10.", minutesdate=NOW(), minutes=".$text6." where clid like '%2451'",$link);
$row = mysql_query("update `lines` set balance=".$text10.", minutesdate=NOW(), minutes=".$text7." where clid like '%2536'",$link);
$row = mysql_query("update `lines` set balance=".$text10.", minutesdate=NOW(), minutes=".$text8." where clid like '%2581'",$link);
$row = mysql_query("update `lines` set balance=".$text10.", minutesdate=NOW(), minutes=".$text9." where clid like '%4056'",$link);

$n[1] = $text2+0;
$n[2] = $text3+0;
$n[3] = $text4+0;
$n[4] = $text5+0;
$n[5] = $text6+0;
$n[6] = $text7+0;
$n[7] = $text8+0;
$n[8] = $text9+0;

$p[1] = 1;
$p[2] = 2;
$p[3] = 3;
$p[4] = 4;
$p[5] = 5;
$p[6] = 6;
$p[7] = 7;
$p[8] = 8;

for($i=1;$i<=7;$i++) for($j=$i+1;$j<=8;$j++)
if ($n[$i]<$n[$j])
{
$t = $n[$i];
$n[$i] = $n[$j];
$n[$j] = $t;

$t = $p[$i];
$p[$i] = $p[$j];
$p[$j] = $t;
}

system('echo "'.$p[1].'" >%pathtoscript%/out1.txt');
system('echo "'.$p[2].'" >%pathtoscript%/out2.txt');
system('echo "'.$p[3].'" >%pathtoscript%/out3.txt');
system('echo "'.$p[4].'" >%pathtoscript%/out4.txt');
system('echo "'.$p[5].'" >%pathtoscript%/out5.txt');
system('echo "'.$p[6].'" >%pathtoscript%/out6.txt');
system('echo "'.$p[7].'" >%pathtoscript%/out7.txt');
system('echo "'.$p[8].'" >%pathtoscript%/out8.txt');

system('asterisk -rx "dialplan reload"');
?>

Содержимое lib.php (там подключение к MySQL):

<?php

$dbuser = "*******";
$dbpass = "*******";
$dbserv = "localhost";
$dbname = "*******";

$dbengn = "mysql";

function sql_connect($host, $login, $password, $dbname)
{
    $link = mysql_connect($host, $login, $password, true);
    mysql_select_db($dbname, $link);
    mysql_query("SET NAMES cp1251");
    return $link;
}

function sql_query($query, $link)
 {
 $res = mysql_query($query, $link);
 return $res;
}

function sql_fetch_assoc($result)
 {
    return mysql_fetch_assoc($result);
 }

$link = sql_connect($dbserv, $dbuser, $dbpass, $dbname);
?>

Еще небольшие пояснения.

На каждой симке делается 10 попыток проверить баланс (это не всегда удается), если хотя бы на одной не получилось, выходим и пробуем через час.

Почему text10=«0»? Раньше использовались симки МТС, которые возвращали баланс (в рублях). В Теле2 используется общий счет, на конкретные симки мы денег не кладем, поэтому там всегда 0.

Балансы также дописываются в файл balance.txt, чтобы можно было посмотреть историю и посчитать статистику трат.

Кому интересно, как не потерять входящий звонок на симку, если по ней уже разговаривают, спросите в комментариях (на статью не тянет).

Спасибо за внимание!
Поделиться с друзьями
-->

Комментарии (40)


  1. eyeofhell
    20.12.2016 09:42

    Выглядит очень сложно. Понимаю, что я как сотрудник Vxoimplant предвзят, но что мешает использовать облачную телефонию для исходящих звонков, звонить из облака безо всякой «симки» и «номера», а при подключении раскидывать звонки на CRM, сотовые, SIP — куда угодно? Это вроде и по деньгам дешевле будет, и десяток строк JavaScript вместо конфигурации Asterisk, и GSM шлюз не нужен?


    1. akopylov
      20.12.2016 09:58

      Можете навскидку сказать, сколько стоит исходящая минута по России из облака?


      1. eyeofhell
        20.12.2016 10:04

        Смотря куда звонить. На городские порядка 10 копеек в минуту, на сотовые — зависит от оператора, обычно порядка 2 рублей хотят.


        1. akopylov
          20.12.2016 10:19

          Через симки исходящий по России стоит 30 копеек, в т. ч. на сотовые. Через облако дорого, мы очень много звоним.


          1. eyeofhell
            20.12.2016 10:21

            А, тада норм.


        1. Raian13
          20.12.2016 11:04

          Нехорошо обманывать. Звонки по Москве/Питеру у вас по прайс-листу 95 коп/минута, по России — от 95 коп до 4,44 рублей. И лишь предположительно бесплатный 8-800 — по 10 коп.


          1. eyeofhell
            20.12.2016 11:08

            Конечно нехорошо, кто спорит. И где вы такие цены нашли? О_О Я обычно на voximplant.ru смотрю, там у нас простой «однокликовый» прайс по кейсам. Есть еще полный на voximplant.com, но оно как бы одним генератором генерируется :)


            1. Raian13
              20.12.2016 12:12

              Именно там и нашла: https://voximplant.ru/pricing.html -> Звонки по России -> Скачать все цены в CSV (http://voximplant.com/docs/rates/voximplant_outbound_rates_RUR.csv)
              Т.е. на самой странице красиво написано, да, но когда по * переходишь на полный прайс — реальность оказывается более приближенной к «земным» ценам. У нас звонки в основном по областным городам России (Ярославль — 74852, Иваново 74932, Пермь 7342, для примера), так ваши цены вполне обычные (ну, подешевле Ростелекома, будем честны).
              Мультифон от Мегафона нам выходит дешевле, по 1.5 руб/минуту по России.


    1. xomiakba
      20.12.2016 10:01

      Ну это реализация конкретного админа, причем, да, очень сложная.

      Данный функционал это 10 строчек на перл/с/с++, и минимальная правка диалплана.

      lib.php — не нужен
      getbalance.php — переложить на цикл, запускать по крону. Убрать вот это: if ($s>10) exit;, ибо если будет проблема с конкретной симкой (блокировка например) — скрипт обречен.

      и тд.


  1. xomiakba
    20.12.2016 09:50
    +1

    exten => _X.,n,set(SIMNUM1=${SHELL(/usr/bin/php -f %pathtoscript%/getsim1.php)})
    exten => _X.,n,set(SIMNUM2=${SHELL(/usr/bin/php -f %pathtoscript%/getsim2.php)})
    exten => _X.,n,set(SIMNUM3=${SHELL(/usr/bin/php -f %pathtoscript%/getsim3.php)})
    exten => _X.,n,set(SIMNUM4=${SHELL(/usr/bin/php -f %pathtoscript%/getsim4.php)})
    exten => _X.,n,set(SIMNUM5=${SHELL(/usr/bin/php -f %pathtoscript%/getsim5.php)})
    exten => _X.,n,set(SIMNUM6=${SHELL(/usr/bin/php -f %pathtoscript%/getsim6.php)})
    exten => _X.,n,set(SIMNUM7=${SHELL(/usr/bin/php -f %pathtoscript%/getsim7.php)})
    exten => _X.,n,set(SIMNUM8=${SHELL(/usr/bin/php -f %pathtoscript%/getsim8.php)}


    WTF?

    Мало того, что PHP, так еще и разными однотипными скриптами, так еще и куча вызовов.

    exten => _X.,n,Set(ARRAY(SIM1,SIM2,SIM3,SIM4)=${SHELL(/usr/bin/php -f %pathtoscript%/getsim.php)})

    Ну или на край использовать аргументы при вызове скрипта.

    Кроме того, какой смысл в lib.php?


  1. akopylov
    20.12.2016 10:00

    Спасибо, одним скриптом конечно лучше.

    В конкретном случае смысла в lib.php нет, но он используется во многих скриптах, чтобы не писать одно и то же.

    Ошибся веткой.


    1. xomiakba
      20.12.2016 10:04

      Есть замечательная штука — unixODBC, рекомендую. Необходимость в костылях отпадет, и доступ к нужной базе будет всего одной строчкой.


    1. agic
      21.12.2016 12:36

      а использовать БД напрямую с астериска?


      1. akopylov
        21.12.2016 12:38

        Я использую БД для вывода текущих остатков.

        Если вы имеете в виду записывать туда порядок вызова симок, то накладные расходы будут больше, чем записать и считать 1 байт из файлов.


        1. xomiakba
          21.12.2016 12:48

          -del-


          1. akopylov
            21.12.2016 12:51

            Чтобы считать номер первой симки, как минимум с диска прочитается запрос «select simnumber from...», который сам по себе занимает несколько десятков байт.

            Плюс найти в БД это поле и считать его значение.


            1. xomiakba
              21.12.2016 12:58

              Мягко говоря спорный вопрос.
              Базы данных как раз и создаются для более эффективного управления данными, нежели обычные файлы.

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


        1. agic
          21.12.2016 12:50

          создайте хранимую процедуру и вам база сама скажет, через какую симку звонить. это гораздо проще, чем у вас.


          1. akopylov
            21.12.2016 12:54

            Ответил в ветке выше, обращение к базе накладнее, чем считать один байт.


            1. agic
              21.12.2016 12:58

              да я не против. как скажите. если вы считаете, что это оптимально, плодить форк то пожалуйста.


      1. xomiakba
        21.12.2016 12:48

        Плохая практика.
        Если база ляжет или неверный пароль — подвесите астериск.


        1. agic
          21.12.2016 12:51

          ого какие есть мнения


          1. xomiakba
            21.12.2016 13:01

            ППД пишется кровью, а такие мнения головной болью админа.
            Я Вам могу еще одно мнение дать: НИКОГДА не храните astdb на NFS.


  1. solalex
    20.12.2016 10:16

    Если есть freepbx, то можно использовать модуль trunk balance. Или еще есть скрипт через функцию RAND.


    1. xomiakba
      20.12.2016 10:42

      RAND не даст балансировку по финансам на симках, правильно все же как у топик стартера. Правда реализация подкачала, но мысль у него верная.


  1. Isiirk
    20.12.2016 10:55

    Мне интересно «как не потерять входящий звонок на симку, если по ней уже разговаривают, спросите в комментариях (на статью не тянет)»


    1. akopylov
      20.12.2016 11:10

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

      Вместо этого мы купили симку Мегафона, на которой есть Мультифон. Зарегистрировали Мультифон на Астериске и на всех симках настроили безусловную переадресацию на этот номер.

      Астериск принимает звонок на Мультифон, в течение 1,5 секунд клиенту проигрывается сообщение «Мы перезвоним», кладется трубка и создается callback. Операторы перезванивают.

      Так как мы укладываемся в 3 секунды, за звонок клиент не платит.


      1. alexsibtone
        22.12.2016 07:11
        +1

        Есть сигнализация pre_answer (использую во FreeSWITCH). Абоненту можно проигрывать голосовые сообщения (до 20 сек) + принимать тоновые нажатия клавиш. Входящие в этом случае бесплатны ибо считаются ожиданием подъема трубки вызываемого абонента.
        p.s. Часто музыка вместо гудков именно по такому принципу делается.


        1. xomiakba
          22.12.2016 09:41

          EarlyMedia?

          Это Ring + SDP, это когда заставляете удаленную сторону думать что будете генерировать гудки сами, а вместо этого генерируете свой контент. Не везде работает кстати, например, если оператор переопределяет сообщения 180/183. Нет гарантии, что вас услышат вообщем.


    1. akopylov
      20.12.2016 11:15

      Также переадресованный вызов бесплатен, так как укладывается в 3 сек.

      Входящие на Мультифоне бесплатны, нужно только подкидывать пару рублей раз в 2-3 месяца и делать платный звонок, чтобы симка не заблокировалась.


    1. xomiakba
      20.12.2016 12:00

      Никак.
      Если нагрузка маленькая, смириться или костыль, описанный выше akopylov.
      А если нагрузка большая, то договорись с оператором о многоканальном СИП с сотового номера (оператор агрегатирует на своей стороне звонки на сотовые номера и выдает в единый транк по SIP, таким образом не важно сколько звонков приходит на один сотовый номер).
      Операторы конечно ломаются — это доп нагрузка на их оборудование, но если хорошо нажать на глаза — предоставляют.


      1. Isiirk
        21.12.2016 03:12

        Мне тоже видится с оператором как то интересней и проще договориться, а то в продакшене вынимать позвонить симки и докинуть 2-3 рубля напрягают… Но каждому этапу развития компании свое решение, на какой то стадии развития и это хороший вариант.


        1. akopylov
          21.12.2016 07:13

          Зачем вынимать позвонить симки?


          1. Isiirk
            21.12.2016 08:43

            Это я так образно, имелось ввиду от этом помнить и следить


    1. willyd
      21.12.2016 07:12

      Переадресацией на симку этого же оператора, если конечно тариф позволяет.


    1. akopylov
      28.12.2016 15:27

      После внедрения колбека стало поступать много мусорных звонков.

      Поэтому теперь вместо колбека шлем смс с названием организации и правильным номером, на который нужно звонить.


      1. xomiakba
        28.12.2016 16:09

        Клиенты уже «оценили» сервис? :)


        1. akopylov
          29.12.2016 07:18

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


  1. KAPMAH
    21.12.2016 07:11

    О боже, вот это костыль)
    в addpac 2000 это делается одной строчкой в конфиге!


    1. alexkirs
      21.12.2016 08:14

      OpenVox VS-GW1600-20G. В web интерфейсе
      1. назначаешь лимиты на каждую sim карту.
      2. устанавливаешь политику fewest call на группу sim-карт
      Минуты на sim расходуются равномерно.
      Минус ежемесячное сбрасывание лимита не знает сколько дней в месяце (всегда 30) — приходится корректировать вручную