Asterisk + Huawei E1550 или как не стоит экономить на телефонии

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

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

В первый же день мне показали существующую систему телефонии. 5 шлюзов VoIP Audiocodes MP-202B и десяток DECT телефонов+ отдельный SIP транк на каждом телефоне. Этими-то телефонами и жонглировали 30 человек весь день. Чё за …, подумал я и решил поставить Asterisk.

В процессе обсуждения с руководством новой системы телефонии было решено взять этих самых Huawei E1550 несколько штук, т.к. было значительно дешевле купить 4 модема, чем GSM шлюз на 4 канала. Это и была моя ошибка по неопытности. Это казалось временным решением, но мы то с вами знаем, что нет ничего более постоянного, чем временное. И тут я познал боль. Боль что:

  • Модемы вставленные в материнскую плату бок о бок кол-вом 4 штуки работать нормально не будут, потому что во первых: не на всех хватает тока (нужно каждому модему 500mA на порт, а материнка не справляется), а во-вторых: судя по всему модемы между собой интерферируют, что тоже не есть гуд.
  • Не все USB хабы одинаково хороши, и далеко не каждый производители впаивает все конденсаторы. Как итог, я купил STLab U-340 (вообще планировал купить D-Link DUB-H7, но его на тот момент нигде не было). Подключил его к серверу, ткнул модемы в него — и ничего не поменялось, так и остались пропадания речи и периодические отваливания модемов. Первое что я сделал, это разобрал его, и увидел, что нифига там нет конденсаторов. Купил. Впаял. Подключил. Поулыбался. Работает. Лучше. Не идеально, но намного лучше, отпадания минимизировались основательно, слышно стало лучше(позже я еще прикупил USB удлинителей 15см, чтоб рознести модемы друг от друга).

И вот когда это все заработало, нужно было уже что-то решать с тем, как балансировать исходящие звонки, т. к. минут на каждом модеме было по 1000, а нам же нужно что б расходовалось равномерно.

С самого начала у меня был стандартный диалплан. Неудобно ИМХО. Особеннко после того, как я набрался опыта в сисадминистрировании, подучил Python для автоматизации, и… познакомился с Lua. Я переписал весь диалплан на Lua и он с 650 строк превратился в 200 с лишним. И писать диалплан на Lua очень удобно, советую.

Но вот решение по балансировке исходящих вызовов я решил написать на Python3.4.

Суть в чем. Есть карточки от МТС. Я подключил Виртуальный менеджер, что бы можно было проверять кол-во минут на каждой карточке. Так вот скрипт ходит по Cron’у на сайт, парсит его, вытаскивает значения минут и пишет в SQLite базу. Вот что происходит в диалплане: в момент набора номера сотрудником из базы вытаскивается название карточки, на которой больше всего минут, и через нее совершается звонок.

Вот пример кода Pyhton скрипта, которых ходит на сайт и парсит минуты:

#!/usr/bin/env python3.4 import requests, bs4, threading, sys, sqlite3, smtplib, os numbers_dic = {                 # <имя канала в Астериске> что то типа mts1 из строчки Dongle/mts1 в диалплане                 #все ID я брал из сайта с помощью FireBug                 #дл примера  '111111':'mts1'                '<конкретный_id_для канала>':'<имя канала в Астериске>',                  '<конкретный_id_для канала>':'<имя канала в Астериске>',                  '<конкретный_id_для канала>':'<имя канала в Астериске>',                '<конкретный_id_для канала>':'<имя канала в Астериске>',                '<конкретный_id_для канала>':'<имя канала в Астериске>'  } bill_info_dic={} bill_info = [] # словарь в котором будут храниться ключами имя каналов, а значениями кол-во минут minutes_lines={} # авторизация на сайте session = requests.session() session.post('https://manager.mts.ua/Ncih/Security.mvc/LogOn', { # это как бы ваш URL для логина     'Name': '<your_login_name>',     'Password': '<your_password_name>',     'remember': 1, }) # главная страница в Виртуальном менеджере url="https://manager.mts.ua/Ncih/ObjectInfo.mvc/Phone" headers = {     'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:42.0) Gecko/20100101 Firefox/42.0',     'Accept': '*/*',     'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',     'X-Requested-With': 'XMLHttpRequest',     'Referer': 'https://manager.mts.ua/Ncih/Hierarchy.mvc' } # метод который ходит на сайт и тащит мне весь биллинг по аккаунту def request_to_billing():     url = "https://manager.mts.ua/Ncih/ObjectInfo.mvc/PersonalAccount"     data_obj = {       'objectId': '<some_id>' #все ID я брал из сайта с помощью FireBug     }     content = session.get(url, data=data_obj, headers=headers)     parsed = bs4.BeautifulSoup(content.content,'html.parser')     # вот тут я из конкретных <td> вытаскиваю инфу по биллингу     td_blocks = parsed.select('td')     balance=(td_blocks[1].getText().split()[0])     waste_1_of_month=(td_blocks[5].getText().split()[0])     sum_of_last_pay=(td_blocks[9].getText().split()[0])     calculate_period_balance=(td_blocks[17].getText().split()[0])     # заменяем запятые на точки, и заносим все в словарь bill_info_dic     bill_info_dic[float(balance.replace(",", "."))]="Balance"     bill_info_dic[float(waste_1_of_month.replace(",", "."))]="Spented from 1st of current month"     bill_info_dic[float(sum_of_last_pay.replace(",", "."))]="Sum of last pay"     bill_info_dic[float(calculate_period_balance.replace(",", "."))]="Balance at the beginning of the calculation period"     # пишем все в массив bill_info     bill_info.append(balance)     bill_info.append("Balance")     bill_info.append(waste_1_of_month)     bill_info.append("Spented from 1st of current month")     bill_info.append(sum_of_last_pay)     bill_info.append("Sum of last pay")     bill_info.append(calculate_period_balance)     bill_info.append("Balance at the beginning of the calculation period") # метод который ниже вызывается в цикле в отдельном треде, ходит на сайт и тащит мне минуты    def request_to_min(num_id):     url = "https://manager.mts.ua/Ncih/ObjectInfo.mvc/Phone"     data_obj = {       'objectId': num_id     }     content = session.get(url, data=data_obj, headers=headers)     parsed = bs4.BeautifulSoup(content.content,'html.parser')     span_blocks = parsed.select('span')     minutes = (span_blocks[2].getText())     sminutes = minutes.split()     minutes_lines[numbers_dic[num_id]]=float(sminutes[1].replace(",", ".")) # тут идет проверка на аргументы командной строки (инструкция есть в конце скрипта) if ((len(sys.argv) > 1) and (sys.argv[1] == "billing")):     request_to_billing()     # создаем sqlite базу     conn = sqlite3.connect('/etc/asterisk/scripts/asterisk_dp.db')     curs = conn.cursor()     # создаем таблицу     curs.execute('CREATE TABLE IF NOT EXISTS mts_billing(id INTEGER PRIMARY KEY, money REAL, description VARCHAR(50))')     conn.commit()     # если хотите почистить базу, есть соотвествующие ключи для скрипта del и new     if ((len(sys.argv) > 2) and (sys.argv[2] == "del")):       curs.execute('DELETE FROM mts_billing')       conn.commit()     n_bill = 1     for money, descr in bill_info_dic.items():         if ((len(sys.argv) > 2) and (sys.argv[2] == "new")):           curs.execute('INSERT INTO mts_billing VALUES (NULL, %f, "%s")' % (money, descr))         else:           curs.execute('UPDATE mts_billing set money = %f, description = "%s" WHERE id = %d' % (money, descr, n_bill))         n_bill += 1     conn.commit()     conn.close() # тут ключ billing_mail отправляет мне на почту инфу  elif ((len(sys.argv) > 1) and (sys.argv[1] == "billing_mail")):     request_to_billing()     conn = sqlite3.connect('/etc/asterisk/scripts/asterisk_dp.db')     curs = conn.cursor()     curs.execute("SELECT * FROM mts_minutes")     conn.commit()     d = curs.fetchall()     msg = 'From:xxx@xxx.com\n' \           'Subject:GSM BILLING FROM ASTERISK\n\n' \           '%s => %s\n%s => %s\n%s => %s\n%s => %s\n%s\n' % (bill_info[0],bill_info[1],bill_info[2],                                                         bill_info[3],bill_info[4],bill_info[5],                                                         bill_info[6],bill_info[7],d)     sender_addr = 'xxx@xxx.com'     rcpt_addr = 'yyy@gmail.com'     smtpobj=smtplib.SMTP_SSL('<ip or domain name of mail server>')     smtpobj.ehlo()     smtpobj.login('xxx@xxx.com', '<password>')     smtpobj.sendmail(sender_addr, rcpt_addr, msg) # ключ minutes пишет кол-во минут в базу elif ((len(sys.argv) > 1) and (sys.argv[1] == "minutes")):     threads = []     for id in numbers_dic:       thrd = threading.Thread(target=request_to_min, args=(id,))       thrd.start()       threads.append(thrd)     for t in threads:       t.join()     conn = sqlite3.connect('/etc/asterisk/scripts/asterisk_dp.db')     curs = conn.cursor()     curs.execute('CREATE TABLE IF NOT EXISTS mts_minutes(id INTEGER PRIMARY KEY, minutes REAL, number VARCHAR(15))')     conn.commit()     if ((len(sys.argv) > 2) and (sys.argv[2] == "del")):       curs.execute('DELETE FROM mts_minutes')       conn.commit()     n_min = 1     for num in sorted(minutes_lines, reverse=True, key=lambda num: minutes_lines[num]):         if ((len(sys.argv) > 2) and (sys.argv[2] == "new")):           curs.execute('INSERT INTO mts_minutes VALUES (NULL, %f, "%s")' % (minutes_lines[num], num))         elif ((len(sys.argv) > 2) and (sys.argv[2] == "show")):           print("min: %s => number: %s" % (minutes_lines[num], num))         else:           curs.execute('UPDATE mts_minutes set minutes = %f, number = "%s" WHERE id = %d' % (minutes_lines[num], num, n_min))         n_min += 1     conn.commit()     conn.close()     msg = 'From:xxx@xxx.com\nSubject:MINUTES\n\n%s' % sorted(minutes_lines)     sender_addr = 'xxx@xxx.com'     rcpt_addr = 'yyy@gmail.com'     smtpobj=smtplib.SMTP_SSL('<ip or domain name of mail server>')     smtpobj.ehlo()     smtpobj.login('xxx@xxx.com', '<password>')     smtpobj.sendmail(sender_addr, rcpt_addr, msg)  else:   print(" -"*10)   print("    MINUTES REQUEST TOOL", end='\n'*2)   print("    HELP", end='\n'*2)   print("    ARGS")   print("      minutes [del|new] -- запускает скрипт и записывает минуты в таблицу mts_minutes (del очистит таблицу, new запущеный только после del, создаст строки которые будут обновляться")   print("      billing [del|new] -- запускает скрипт и записывает минуты в таблицу mts_billing (del очистит таблицу, new запущеный только после del, создаст строки которые будут обновляться")   print("      billing_mail      -- отправляет на почту биллинг")   print(" -"*10) 

Я не особо пока шарю в программировании, и то что написано выше может показаться кодом не очень хорошим, но это работает + каждый может его переделать. Понятно что этот код работает до тех пор, пока HTML код страницы не поменяется. Ну что ж… это работает для меня. Если у ват другой оператор, другой билинг, то вам понятное дело придется сменить URL’ы и пройтись по HTML коду страницы, что бы понять что и как правильно парсить. Еще мне кстати очень удобно было отлаживать некоторые моменты скрипта в консольной версии питона IPython3, удобно.

Вообще скрипт для себя я делал более функциональным. Он мне на почту и биллинг отсылает, и инфу по минутам. А вот пример который только узнает кол-во минут и пишет их в базу:

#!/usr/bin/env python3.4 import requests, bs4, threading, sys, sqlite3, os  numbers_dic = {                 # <имя канала в Астериске> что то типа mts1 из строчки Dongle/mts1 в диалплане                 #все ID я брал из сайта с помощью FireBug                 #дл примера  '111111':'mts1'                '<конкретный_id_для канала>':'<имя канала в Астериске>',                  '<конкретный_id_для канала>':'<имя канала в Астериске>',                  '<конкретный_id_для канала>':'<имя канала в Астериске>',                '<конкретный_id_для канала>':'<имя канала в Астериске>',                '<конкретный_id_для канала>':'<имя канала в Астериске>'  }  # словарь в котором будут храниться ключами имя каналов, а значениями кол-во минут minutes_lines={} # авторизация на сайте session = requests.session() session.post('https://manager.mts.ua/Ncih/Security.mvc/LogOn', { # это как бы ваш URL для логина     'Name': '<your_login_name>',     'Password': '<your_password_name>',     'remember': 1, }) # главная страница в Виртуальном менеджере url="https://manager.mts.ua/Ncih/ObjectInfo.mvc/Phone" headers = {     'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:42.0) Gecko/20100101 Firefox/42.0',     'Accept': '*/*',     'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',     'X-Requested-With': 'XMLHttpRequest',     'Referer': 'https://manager.mts.ua/Ncih/Hierarchy.mvc' }  # метод который ниже вызывается в цикле в отдельном треде, ходит на сайт и тащит мне минуты    def request_to_min(num_id):     url = "https://manager.mts.ua/Ncih/ObjectInfo.mvc/Phone"     data_obj = {       'objectId': num_id     }     content = session.get(url, data=data_obj, headers=headers)     parsed = bs4.BeautifulSoup(content.content,'html.parser')     span_blocks = parsed.select('span')     minutes = (span_blocks[2].getText())     sminutes = minutes.split()     minutes_lines[numbers_dic[num_id]]=float(sminutes[1].replace(",", ".")) # тут идет проверка на аргументы командной строки (инструкция есть в конце скрипта) # ключ minutes пишет кол-во минут в базу elif ((len(sys.argv) > 1) and (sys.argv[1] == "minutes")):     threads = []     for id in numbers_dic:       thrd = threading.Thread(target=request_to_min, args=(id,))       thrd.start()       threads.append(thrd)     for t in threads:       t.join()     conn = sqlite3.connect('/etc/asterisk/scripts/asterisk_dp.db')     curs = conn.cursor()     curs.execute('CREATE TABLE IF NOT EXISTS mts_minutes(id INTEGER PRIMARY KEY, minutes REAL, number VARCHAR(15))')     conn.commit()     if ((len(sys.argv) > 2) and (sys.argv[2] == "del")):       curs.execute('DELETE FROM mts_minutes')       conn.commit()     n_min = 1     for num in sorted(minutes_lines, reverse=True, key=lambda num: minutes_lines[num]):         if ((len(sys.argv) > 2) and (sys.argv[2] == "new")):           curs.execute('INSERT INTO mts_minutes VALUES (NULL, %f, "%s")' % (minutes_lines[num], num))         elif ((len(sys.argv) > 2) and (sys.argv[2] == "show")):           print("min: %s => number: %s" % (minutes_lines[num], num))         else:           curs.execute('UPDATE mts_minutes set minutes = %f, number = "%s" WHERE id = %d' % (minutes_lines[num], num, n_min))         n_min += 1     conn.commit()     conn.close() 

Вот пример диалплана на Lua который берет значения из базы (Asterisk кстати скомпилирован с Lua версии 5.2):

local sqlite3 = require("lsqlite3") -- функция которая ходит в базу и берет номер, через который будет совершаться звонок function gsm_outgoing(context, extension)   local db = sqlite3.open("/etc/asterisk/scripts/asterisk_dp.db", "wc")   -- kyivstar section   -- номера которые Киевстар, вызываются через один канал Dongle/kyivstar, в базу ходить не надо   if ((extension:sub(1,3) == extension:sub(1,3):match('06[7,8]')) or (extension:sub(1,3) == extension:sub(1,3):match('09[6,7,8]'))) then     app.dial(string.format("Dongle/kyivstar/%s, 45 tkr", extension))     local state = channel.DIALSTATUS:get()     -- но канал Dongle/kyivstar один, и если он занят, то мы все же позвоним через один из МТС     if (state == "CHANUNAVAIL") then      for _, _, c in db:urows("SELECT * FROM mts_minutes") do      app.noop("Calling through "..c)      app.dial(string.format("Dongle/%s/%s, 45 tkr", c, extension))      local state = channel.DIALSTATUS:get()      if (state ~= "CHANUNAVAIL") then        app.noop("Device "..c.." in  status "..state)        break      else        app.noop("Device "..c.." in  status "..state)      end;      end;     end;     app.hangup()     ---mts section   else     for _, _, c in db:urows("SELECT * FROM mts_minutes") do      app.noop("Calling through "..c)      app.dial(string.format("Dongle/%s/%s, 45 tkr", c, extension))      local state = channel.DIALSTATUS:get()      if (state ~= "CHANUNAVAIL") then        app.noop("GOOD ! Device "..c.." in  status "..state)        break      else        app.noop("So sad ! Device "..c.." in  status "..state)      end;     end;   app.hangup()   end; end;  extensions = {  -- Исходящие звонки МТС    gsm = {     ["_03[1,2,3,4,5,6,7,8]XXXXXXX"] = gsm_outgoing;--mts     ["_04[1,2,3,5,6,7,8,9]XXXXXXX"] = gsm_outgoing;--mts     ["_05[0,1,2,3,4,5,6,7,8,9]XXXXXXX"] = gsm_outgoing;--mts     ["_06[1,2,3,4,5,6,9]XXXXXXX"] = gsm_outgoing;--mts     ["_07[3]XXXXXXX"] = gsm_outgoing;--life     ["_09[1,2,3,4,5,9]XXXXXXX"] = gsm_outgoing;--mts     ["_06[7,8]XXXXXXX"] = gsm_outgoing;--kyivstar     ["_09[6,7,8]XXXXXXX"] = gsm_outgoing;--kyivstar   }; } 

Резюмируя, по поводу системы телефонии в общем, то да, Asterisk — круто и возможностей у него много, очень много. Но вот лепить туда такое как GSM ​модемы, я не советую. Да! Оно работает, и при умении пользоваться паяльником, оно работает еще лучше, но… Оно вам надо? Покупайте нормальные шлюзы. У меня все. Все пис!

ссылка на оригинал статьи http://habrahabr.ru/post/273661/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *