python

Действительно полезное приложение для Digium телефонов

  • суббота, 9 апреля 2016 г. в 03:16:10
https://habrahabr.ru/post/281146/
  • Совершенный код
  • Разработка систем связи
  • Python
  • JavaScript
  • Asterisk


image

Приветствую, хабрасообщество.

Чуть более года назад мы разрабатывали приложения для Digium телефонов. Несмотря на то, что планы были обширными, мы остановились только на следующих вариациях:

  • Погода с сайта гисметео
  • Курс валют с сайта центробанка
  • RSS лента с новостных порталов


Данные приложения были написаны, чтобы ознакомить сообщество с API и примерами, даже больше just for fun. Cофт, если так можно его назвать, не несет себе никакого уникального применения, которое было бы полезно реальному бизнесу.

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

За подробностями — > хабракат


Ввиду того, что телефон сам не может обратиться к астериску по HTTP, была выбрана структура клиент-сервер. Сервер на питоне, который «ловит» от астериска CURL запросы при звонке, и javascript на телефоне, который с некоторой периодичностью опрашивает о наличии новых записей.

server.py
from BaseHTTPServer import BaseHTTPRequestHandler,HTTPServer

import urlparse

import json

import shelve

import datetime

import sys

import os



TIME_FORMAT = '%d.%m.%Y %H:%M:%S.%f'

DB_NAME =  "db.db"


class HttpProcessor(BaseHTTPRequestHandler):

    def do_GET(self):

        if "/get" in self.path:

            fields = self.get_fields()

            if not fields:

                print "There are no parameters"

                return

            callgroup = fields.get("callgroup")

            pickupgroup = fields.get("pickupgroup")

            if callgroup is None or pickupgroup is None:

                print "There are no pickupgroup or no callgroup"

                self.send_error(404)

                return None

            db = shelve.open( DB_NAME)

            data = []

            for key in db.keys():

                if "callgroup" in db[key] and "pickupgroup" in db[key] and str(db[key]["callgroup"]) == str(callgroup) and str(db[key]["pickupgroup"]) == str(pickupgroup):

                    entry = db[key]

                    data.append(entry)

            # Make simple dict for json

            self.send_json(data)

            db.close()



    def do_POST(self):

        data = self.get_json_data()

        if "uid" not in data:

            print "There are no a number"

            return

        uid = str(data["uid"])

        print data

        if "/put" in self.path:

            db = shelve.open( DB_NAME)

            db[uid] = data

            db.close()

            data = {"status": 200, "message": "OK"}

            self.send_json(data)

        elif "/del" in self.path:

            db = shelve.open( DB_NAME)

            if uid in db:

                del db[uid]

            data = {"status": 200, "message": "OK"}

            self.send_json(data)



    def send_json(self, data):

        self.send_response(200)

        self.send_header('Content-Type', 'application/json')

        self.end_headers()

        self.wfile.write(json.dumps(data))



    def get_json_data(self):

        length = int(self.headers.getheader('content-length'))

        field_data = self.rfile.read(length)

        try:

            data = json.loads(field_data)

        except:

            print "JSON error:" + field_data

            self.send_error(404)

            return None

        return data



    def get_fields(self):

        fields_data = self.path.split("?")

        if len(fields_data) < 2:

            return

        GET = {}

        args = fields_data[1].split('&')

        for arg in args:

            t = arg.split('=')

            if len(t) > 1:

                k, v = arg.split('=')

                GET[k] = v

        return GET



if __name__ == "__main__":

    if os.path.exists( DB_NAME):

        os.remove( DB_NAME)

    host = "192.168.1.254"

    port = 8000

    serv = HTTPServer((host, port), HttpProcessor)

    print "Server running at {}:{}".format(host, port)

    serv.serve_forever()




Не забудьте дать этому файлу права на исполнение (chmod +x), запустить и добавить в автозапуск.

Установка приложения на телефон



Как вы наверняка знаете, или слышали, Digium телефон можно настроить с помощью веб-интерфейса, либо с помощью некоего проприетарного провиженинга DPMA. (Digium Phone Module Asterisk).
Если у вас самая простая настройка (через веб), то вам необходимо перейти в меню телефона System tools и нажать Enable App Development, затем залогиниться на телефон по адресу phone_ip-address/app_dev (по дефолту: Юзер: admin, Пароль: 789) и там нажать большую зеленую кнопку Add App.



Вот собственно файл на скачивание самого приложения: pbxware.ru

Сам код
var incomingGroupCall = {}
var screen = require('screen');
//util for debugging
var util = require('util');
var app = require('app');
//we needs to get all info about app
app.init();
screen.clear();

//Get config of app(we needs settings)
var config = app.getConfig();
var callgroup =  config.settings.callgroup; //Get callgroup
var pickupgroup =  config.settings.pickupgroup; //Get pickupgroup
var server = config.settings.server; //server uri, like http://{host}:{port}
var app_name = config.settings.id; //App name from json file
var phonePrefix = config.settings.prefix; //App name from json file
var language = config.settings.language || "ru";

var uids = []; // This list contains all uids server give us at runtime
var phonesCount = 0; // This variable needs to watch uids missing from uids list
var timer;
var currentListPos = 0;
var listWidget = new List(0, 0, window.w, window.h);
var lang = digium.readFile("app", language + ".json");
language = JSON.parse(lang);

incomingGroupCall.show = function () {
	util.debug("Call show");
	if (this.visible) {
		window.add(listWidget);		
	}
	this.update();
	if (timer) {
		clearInterval(timer);
	}
	timer = setInterval(this.update, 1100);
};

incomingGroupCall.showGui = function(message, params) {
	util.debug("Show gui");
	var lastSelected = listWidget.selected;
	listWidget.clear();
	listWidget.set(0,0, message);
	var i=1;
	if(!params){
		return;
	}
	params.forEach(function(entry) {
		var msg = entry.from + " --> " + entry.to;
		if(i<=9){
			msg = "[" + i + "]  " + msg;
		} else if(i === 10) {
			msg = "[0]  " + msg;
		} else if(i === 11) {
			msg = "[*]  " + msg;
		} else if(i === 12) {
			msg = "[#]  " + msg;
		}
		listWidget.set(i, 0, msg);
		listWidget.set(i, 1, entry.to); //container to get value in key handler
		i++;
	});
	listWidget.select(lastSelected);
}


incomingGroupCall.update = function() {
	var request = new NetRequest();
	request.open("GET", server + "/get?callgroup="+callgroup+"&pickupgroup="+pickupgroup);
	request.onreadystatechange = function() {
		//(readyState === 4) indicates a completed request		
		if (4 === request.readyState) {
			if (200 === request.status) {				
				try {				
					var data = JSON.parse(request.responseText);
					if (!data || data.length === 0) {
						if (!digium.app.inForeground) {
							return;
						}
						incomingGroupCall.showGui(language["NO_CALLS"]);
						return;
					}
					//Remove ended calls
					var currentUids = data.map( function(item) { 
						return item.uid; 
					});
					var needsToRefresh = false;
					var newEntryAvailable = false;
					uids.forEach(function(uid) {
						if(currentUids.indexOf(uid) === -1) {
							uids.splice(uids.indexOf(uid), 1);
							needsToRefresh = true;
						}
					});					
					// Add new phones to list				
					data.forEach(function(entry) {
						if (uids.indexOf(entry.uid) === -1) {
							needsToRefresh = true;
							newEntryAvailable = true;
							uids.push(entry.uid);
						}
					});
					util.debug("New entry:" + newEntryAvailable);
					if (!digium.app.inForeground && newEntryAvailable) {
						digium.foreground();
					}
					if(needsToRefresh) {
						incomingGroupCall.showGui(language["INCOMING_CALLS"], data);
					}
					
				} catch (e) {
					util.debug('request error: ' + JSON.stringify(e));
					incomingGroupCall.showGui(language["SERVER_UNAVAILABLE"]);
				}
			} else {
				util.debug('request error1: ' + request.status);				
				incomingGroupCall.showGui(language["SERVER_UNAVAILABLE"]);
			}
		}			
	}.bind(this);
	request.setTimeout(1000);
	request.send();
};

//initialize variables
incomingGroupCall.init = function () {
	this.widgets = {};

	//stay open when the app is backgrounded
	digium.app.exitAfterBackground = false;

	this.visible = digium.app.inForeground;
	incomingGroupCall.listeners();

	// setInterval(digium.restart, 180000);
};


incomingGroupCall.listeners = function () {
	//show the full window when the app is foregrounded
	digium.event.observe({
		'eventName'		: 'digium.app.foreground',
		'callback'		: function () {
			util.debug("app.foregrounded");
			window.clear();
			this.visible = digium.app.inForeground;
			this.setButtons();
			this.show();
		}.bind(this)
	});

	//show the idle window when the idleScreen is shown
	digium.event.observe({
		'eventName'		: 'digium.app.background',
		'callback'		: function () {
			util.debug("app.background");
			this.visible = digium.app.inForeground;
			window.clearSoftkeys();
			this.show();
		}.bind(this)
	});
};

incomingGroupCall.setButtons = function () {
	window.onkeyselect = function() {
		var phone = listWidget.get(listWidget.selected, 1);
		util.debug("Selected " + phone);
		digium.phone.dial({
			"number": phonePrefix+phone
		})
	}
	window.onkey = function(e) {
		//Digits keyboard handler
		try {
			var key = e.key;
			if(e.key == "0") {
				key = 10;
			} else if (e.key == "*") {
				key = 11;
			} else if (e.key == "#") {
				key = 12;
			}
			var phone = listWidget.get(key, 1);
			if (phone) {
				digium.phone.dial({
					"number": phonePrefix+phone
				})
			}
		} catch(e) {
			util.debug("Error in trying to dial");
		}
		util.debug(JSON.stringify(e));
	}
	window.setSoftkey(4, language['EXIT'], function() {
		digium.app.exitAfterBackground = true;
		digium.background();
	}.bind(this));
	window.setSoftkey(3, language['HIDE'], function() {
		digium.background();
	}.bind(this));
}

incomingGroupCall.init();
incomingGroupCall.show();


В настройках нового приложения необходимо указать следующие опции:

callgroup: 1
pickupgroup: 1
server: 192.168.1.254:8000 (ну или любой другой IP, где запущен питоновский скрипт)
prefix: *8 (префик для перехвата звонка)
language: ru (поддерживается en / ru)

Скриншот настройки телефона



Если вы используете DPMA, то необходимо загрузить приложение на телефоны с помощью этой инструкции

Запускаете приложение. Можете оставить открытым, либо свернуть в фон.



Изменение диалплана Asterisk



Итак, приложение мы записали на телефон, ретранслятор на сервере тоже запустили. Осталось внести изменения в диалплан астериска, чтобы он сообщал, что на добавочный номер пришел звонок с нужными нам параметрами callgroup и PickupGroup.

Например, вы можете использовать наш рабочий диалплан

exten => _7XX,1,NoOp(Call from ${CALLERID(num)} to ${EXTEN})
same => n,Set(CallGroup=${SIPPEER(${EXTEN},callgroup)})
same => n,NoOp(Callgroup = ${CallGroup})
same => n,Set(PickupGroup=${SIPPEER(${EXTEN},pickupgroup)})
same => n,NoOp(PickupGroup = ${PickupGroup})
same => n,System(curl -i -H «Accept: application/json» -H «Content-Type: application/json» -X POST -d '{«uid»:"${UNIQUEID}", «callgroup»:"${CallGroup}", «pickupgroup»:"${PickupGroup}", «from»:"${CALLERID(num)}", «to»:"${EXTEN}"}' 192.168.1.254:8000/put)
same => n,Dial(SIP/${EXTEN},60,Tt)
same => n,Set(CallGroup=${SIPPEER(${EXTEN},callgroup)})
same => n,Hangup()

exten => h,1,NoOp(END of App)
same => n,System(curl -i -H «Accept: application/json» -H «Content-Type: application/json» -X POST -d '{«uid»:"${UNIQUEID}", «callgroup»:"${CallGroup}", «pickupgroup»:"${PickupGroup}", «from»:"${CALLERID(num)}", «to»:"${TARGETNO}"}' 192.168.1.254:8000/del)
same => n,Hangup()

Приложение в действии





При входящем звонке на экране телефона всплывает наше приложение, которое показывает, звонки в данной колл-группе и позволяет его перехватить. Никакой другой сигнализации нет (например мелодии или световой индикации)

По номерам можно перемещаться с помощью кнопок «вверх» и «вниз», можно перехватить вызов нажатием на кнопку «ОК», тогда перехватится выделенный номер, либо с помощью цифр и *, #.

Запускаешь приложение, сворачиваешь его и ждешь, когда позвонят). При новом звонке приложение развернется. Можно опять свернуть его.