Developers Club geek daily blog

2 years, 9 months ago
When I one and a half years ago, being a student 4 courses of telecommunications, came to work to the company for the system administrator's position, I understood what works I will have much, and it is necessary to teach all new even more. I pushed study the background, and then began to appear on couples of times at all a month because there is a lot of work, and it was precisely more interesting than what tried to teach in University to.

The company was engaged in sales, and naturally there were many such little men who are called sales managers, and they needed to call much!

In the first day showed me the existing system of telephony. 5 gateways VoIP Audiocodes MP-202B and ten DECT of phones + separate SIP a trunk on each phone. With these phones 30 people also juggled all day. That for..., I thought and decided to deliver to Asterisk.

In the course of discussion with the manual of new system of telephony was decided to take these Huawei E1550 several pieces since it was much cheaper to purchase 4 modems, than the GSM gateway on 4 channels. It was also my error by inexperience. It seemed makeshift, but we that with you know that there is nothing more permanent, than temporary. And here I learned pain. Pain that:

  • Modems 4 pieces inserted into the motherboard by quantity will normally not work side by side because first: not for all there is enough current (it is necessary for each modem 500mA on the port, and the materinka does not cope), and secondly: apparently modems among themselves interfere that too there is no buzzing.
  • Not everything hubs are equally good USB, and not everyone vendors seals all condensers. As a result, I purchased STLab U-340 (in general was going to purchase D-Link DUB-H7, but it at that time was not anywhere). Connected it to the server, stuck modems into it — and nothing exchanged, and there were losses of the speech and periodic otvalivaniye of modems. The first that I made, it sorted it, and saw that there is no nifig condensers there. Purchased. Sealed. Connected. It Poulybatsya. Works. Better. Not ideally, but it is much better, falling off were minimized thoroughly, it became heard better (later I still bought cm USB extenders 15 to carry modems from each other).

And here when all this earned, it was already necessary to decide something how to balance outgoing calls since minutes on each modem was on 1000, and it is necessary for us that it was spent evenly.

From the very beginning at me was standard dialplan. IMHO is inconvenient. Osobennko after I accumulated experience in a sisadministrirovaniye, learned Python for automation, and … got acquainted with Lua. I rewrote all dialplan on Lua and it from 650 lines turned in more than 200. And it is very convenient to write dialplan on Lua, I advise.

But here I decided to write a solution on balancing of outgoing calls on Python3.4.

Essence in what. There are cards from MTS. I connected the Virtual manager that it was possible to check number of minutes on each card. And so the script goes on Cron'U to the website, parsit it, pulls out values of minutes and writes base to SQLite. Here that occurs in the dialplena: at the time of number set the employee from base pulls out the name of a card on which most of all minutes, and through it are made a call.

Here a script Pyhton code sample which goes to the website and parsit minutes:

#!/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)

I not especially so far rummage in programming, and the fact that is written above can seem a code not really good, but it works + everyone can change it. It is clear that this code works until the HTML code of the page does not exchange. Well … it works for me. If at watts other operator, other billing, then you clear business have to replace URL'Y and to walk on a page HTML code to understand as as it is correct to parsit. Still by the way it was very convenient to me to debug some moments of a script in the console version of a python of IPython3, conveniently.

In general for myself I did a script to more functional. It on mail and billing sends me, and an inf on minutes. And here an example which only learns number of minutes and writes them to base:

#!/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()

Here an example of a dialplan on Lua which takes values from base (Asterisk by the way is compiled with version 5.2 Lua):

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
  };
}

Summarizing, concerning system of telephony generally, yes, Asterisk — abruptly and opportunities at it is a lot of, it is a lot of. But here to mold there it as modema GSM, I do not advise. Yes! It works, and at ability to use a soldering iron, it works even better, but … It is necessary to you? Buy normal gateways. I have all. In total pis!

This article is a translation of the original post at habrahabr.ru/post/273661/
If you have any questions regarding the material covered in the article above, please, contact the original author of the post.
If you have any complaints about this article or you want this article to be deleted, please, drop an email here: sysmagazine.com@gmail.com.

We believe that the knowledge, which is available at the most popular Russian IT blog habrahabr.ru, should be accessed by everyone, even though it is poorly translated.
Shared knowledge makes the world better.
Best wishes.

comments powered by Disqus