JavaScript для… настольного IP телефона?

в 10:57, , рубрики: asterisk, digium, ip-телефония, javascript

JavaScript для… настольного IP телефона?

Немногим известно, что IP телефоны Digium — это не совсем обычные телефоны. Казалось бы зачем производителю выпускать аппараты собственной марки на таком насыщенном и низкомаржинальном рынке? Но поверьте — оно того стоило. Помимо отличных физических характеристик: приятный пластик, яркий экран, отличные динамики и микрофон. Эти аппараты обладают своим API и вы можете написать свое приложение под них!

Кто другой может похвастаться таким функционалом?


JavaScript для… настольного IP телефона?

Написание собственного приложения под новую платформу это не только интересно и познавательно, но и полезно! Я как-то говорил в предыдущем обзоре, что идея приложения может ограничиться только вашей фантазией.

Моя фантазия посчитала, что полезными для настольного IP телефона могут оказаться следующие вещи:

1) Синхронизация контактов с Google аккаунтом
2) Синхронизация контактов Facebook, VK
3) Вывод курса валют с ЦБ
4) Оповещения о непрочитанных email (интеграция с почтовиками по POP3)
5) Приложения погоды (с русскоязычных Gismeteo, yandex и т.п.)
6) синхронизация контактов с LDAP
7) приложение, которое дает супервайзеру подслушать или вклиниться в разговор сотрудника.

Последние два пункта — это конечно очень «круто», но боюсь предназначена только для гуру JavaScript и астерискера по совместительству.

Я бы с удовольствием прочел о Ваших идеях таких вот апплетов.

Приложение погоды с GISMETEO

JavaScript для… настольного IP телефона?

Решено было начать с самого простого. Виджета погоды.
Ознакомившись в документацией и примерами получился такой вот код:

Informer.js

// Lets include the app (for standard settings option):
var app = require('app');

// And initialize it as required:
app.init();

// Our select tool input:
var SelectInput = require('SelectInput').SelectInput;

// Include some utilities:
var util = require('util');

// Screen instance:
var screen = require('screen');

// Idle window reference;
var wnd = null;

// On foreground event call:
var onforeground_called = false;

// On foreground event call to display error:
var onforeground_error = false;

// If app is already running it has true value:
var instantiated = false;

// Default city for "no-config" app:
var DEF_CITY_ID = 27612;

// Screen update interval (for 4 different forecasts)
var DEF_FORECAST_SCREEN_INT = 15000;

// Server request interval (to update weather data):
var DEF_FORECAST_UPDATE_INT = 3600000;

// Server request interval (to update weather data in case of error):
var DEF_FORECAST_ERROR_UPDATE_INT = 60000;

// App runtime configuration:
var config = {};

// Runtime gathered weather data:
var weather_data = {};

// Which forecast should display next refresh;
var forecast_display = 0;

// Count of available forecasts:
var forecasts_count = 0;

// Timer id of weather update request timeout
var timeout_request = null;

// Timer id of forecast display refresh timeout
var timeout_refresh = null;

// Extended time of day explanations:
var tod_extended = {
	'0': ['Утром', 'Днём', 'Вечером'],
	'1': ['Днём', 'Вечером', 'Ночью'],
	'2': ['Вечером', 'Ночью', 'Завтра утром'],
	'3': ['Ночью', 'Завтра утром', 'Завтра днём']
};

// Weekday russian shortcuts (Gismeteo format):
var week_days = {
	'1':'вс',
	'2':'пн',
	'3':'вт',
	'4':'ср',
	'5':'чт',
	'6':'пт',
	'7':'сб'
};

// Gismeteo wind direction map:
var wind_dir = {
	'0':'C',
	'1':'СВ',
	'2':'В',
	'3':'ЮВ',
	'4':'Ю',
	'5':'ЮЗ',
	'6':'З',
	'7':'СЗ'
};

// Decode CP1251 (name of city):
function decode1251 (str) {
	var i, result = '', l = str.length, cc;

	for (i = 0; i < l; i++) {
		cc = str.charCodeAt(i);

		if ('r' == str[i]) continue;

		if (cc < 192) {
			if (168 == cc)
				result += 'Ё';
			else if (184 == cc)
				result += 'ё';
			else
				result += String.fromCharCode(cc);
		}
		else
			result += String.fromCharCode(cc + 848);
	}

	return result;
}

// Just a little workaround for IDE:
function endForegroundMonitor () {
	onforeground_called	= false;
	onforeground_error		= false;
}

// Show window for selecting user-defined city:
function showFormCities () {
	if (!onforeground_called) return;

	digium.event.stopObserving({'eventName'	: 'digium.app.background'});

	digium.event.observe({
		'eventName'	: 'digium.app.background',
		'callback'	: function () {
			endForegroundMonitor();
			digium.event.stopObserving({'eventName'	: 'digium.app.background'});
		}
	});

	screen.clear();

	var i, select_options = [];

	for (i in cities) {
		if (cities.hasOwnProperty(i))
			select_options.push({'value':i,'display':cities[i]});
	}

	var select = new SelectInput({
		'options':select_options,
		'width':window.w - 120,
		'title':'Выберите город из списка',
		'x':100,
		'y':20+Text.LINE_HEIGHT,
		'initialValue':config['CITY_ID'].toString()
	});

	select.onFocus = function(){return true};

	window.add(screen.setTitleText({'title' : 'Настройка местоположения'}));
	window.add(new Text(20, 20+Text.LINE_HEIGHT, 70, Text.LINE_HEIGHT, 'Ваш город:'));
	window.add(select.textWidget);

	select.takeFocus();

	select.textWidget.setSoftkey(4, 'Назад', showAppWindow);

	select.textWidget.setSoftkey(1, 'Готово', function(){
		config['CITY_ID'] = parseInt(select.value);
		confirmUserCity();
		updateWeatherData();
		digium.background();
	});
}

// Saving user's choice:
function confirmUserCity () {
	try {
		digium.writeFile(
			'nv',
			'settings.json',
			JSON.stringify({'CITY_ID':config['CITY_ID']})
		);
	}
	catch(e) {}
}

// Getting app-level configuration:
function getApplicationConfig () {
	return util.defaults(app.getConfig().settings, {
		'CITY_ID':DEF_CITY_ID,
		'FORECAST_SCREEN_INT':DEF_FORECAST_SCREEN_INT,
		'FORECAST_UPDATE_INT':DEF_FORECAST_UPDATE_INT
	});
}

// Getting user-level configuration:
function getLocalConfig () {
	var result;

	try {
		var configFile = digium.readFile('nv', 'settings.json');

		result = JSON.parse(configFile);
	} catch (e) {
		result = {};
	}

	return result;
}

// Function-helper for parsing single node attrs:
function getMappedAttributes (node, map) {
	var i, result = {};

	for (i in map) {
		if (map.hasOwnProperty(i))
			result[map[i]] = node.getAttribute(i);
	}

	return result;
}

// Parse xml weather data to object:
function parseWeatherData (src) {
	var data = {}, nodes, node, l, i, subnode;

	var parser = new DOMParser();

	try {
		var doc = parser.parseFromString(src, 'application/xml');

		nodes = doc.getElementsByTagName('TOWN');

		data.city = decode1251(unescape(nodes[0].getAttribute('sname'))).replace('+', ' ');

		nodes = doc.getElementsByTagName('FORECAST');

		l = nodes.length; i = 0;

		forecasts_count = l;

		node = nodes[0];

		data.day = node.getAttribute('day');
		data.month = node.getAttribute('month');
		data.weekday = node.getAttribute('weekday');
		data.tod = node.getAttribute('tod');
		data.forecasts = {};

		var forecast, tmp;

		do {
			forecast = {};

			subnode = node.getElementsByTagName('PHENOMENA')[0];

			forecast = util.defaults(forecast, getMappedAttributes(subnode, {
				'cloudiness':'clouds',
				'precipitation':'precip',
				'rpower':'rpower',
				'spower':'spower'
			}));

			subnode = node.getElementsByTagName('PRESSURE')[0];

			tmp =  getMappedAttributes(subnode, {
				'min':'min',
				'max':'max'
			});

			forecast.press = tmp.min + '-' + tmp.max;

			subnode = node.getElementsByTagName('TEMPERATURE')[0];

			tmp =  getMappedAttributes(subnode, {
				'min':'min',
				'max':'max'
			});

			forecast.temp = parseInt((parseInt(tmp.min) + parseInt(tmp.max)) / 2);

			subnode = node.getElementsByTagName('WIND')[0];

			tmp =  getMappedAttributes(subnode, {
				'min':'min',
				'max':'max',
				'direction':'wdir'
			});

			forecast.wspeed = parseInt((parseInt(tmp.min) + parseInt(tmp.max)) / 2);
			forecast.wdir = tmp.wdir;

			subnode = node.getElementsByTagName('RELWET')[0];

			forecast = util.defaults(forecast, getMappedAttributes(subnode, {
				'max':'hum'
			}));

			data.forecasts[i] = forecast;

			node = nodes[++i];
		}
		while (i < l);
	}
	catch(e) {
		data = {error:true};
	}

	return data;
}

// Currently disabled (show error message in window of the app)
/*function reportError (info) {
	endForegroundMonitor();

	onforeground_error = true;

	digium.foreground();

	screen.clear();

	window.add(new Text(0, 0, window.w,Text.LINE_HEIGHT * 2, info.type + ': ' + info.description));

	digium.event.stopObserving({'eventName'	: 'digium.app.background'});

	digium.event.observe({
		'eventName'	: 'digium.app.background',
		'callback'	: function () {
			endForegroundMonitor();
			digium.event.stopObserving({'eventName'	: 'digium.app.background'});
		}
	});
}*/

// Get current no of forecast to display:
function getForecastToDisplay () {
	if (forecast_display > (forecasts_count - 1) )
		return forecast_display = 0;
	else
		return forecast_display++;
}

// Get labels values (with updated data):
function getUpdatedLabels (forecast_no) {
	var result = {}, time = '';

	var forecast = weather_data.forecasts[forecast_no];

	if (!forecast_no) time = 'Сейчас';
	else time = tod_extended[weather_data.tod][forecast_no - 1];

	result.temp = time + ' ' + forecast.temp + ' °C';
	result.press = forecast.press + ' мм рт. ст.';
	result.hum = 'Влажность ' + forecast.hum + '%';
	result.wind = 'Ветер ' + forecast.wspeed + ' м/с ' + wind_dir[forecast.wdir];

	return result;
}

// "Humanize" Gismeteo phenomena:
function getPhenomenaObj (forecast, nt) {
	// Default state of the phenomena part:
	var result = {icon:'unknown',status:'Нет данных'};

	if ((1 == forecast.spower) &&
		((8 == forecast.precip) || (9 == forecast.precip))) {

		result.icon = 'storm';
		result.status = 'Грозы';
	}
	else if (5 == forecast.precip) {
		result.icon = 'rainfall';
		result.status = 'Ливень';
	}
	else if (4 == forecast.precip) {
		result.icon = 'rain';
		result.status = 'Дождь';
	}
	else if ((6 == forecast.precip) || (7 == forecast.precip)) {
		result.icon = 'snow';
		result.status = 'Снег';
	}
	else if (3 == forecast.clouds) {
		result.icon = 'mostlycloudy';
		result.status = 'Пасмурно';
	}
	else if (2 == forecast.clouds) {
		result.icon = 'cloudy';
		result.status = 'Облачно';
	}
	else if ((1 == forecast.clouds) && (10 == forecast.precip)) {
		if (nt)
			result.icon = 'nt_mostlyclear';
		else
			result.icon = 'mostlyclear';

		result.status = 'Перем. облач.';
	}
	else if ((0 == forecast.clouds) && (10 == forecast.precip)) {
		if (nt)
			result.icon = 'nt_clear';
		else
			result.icon = 'clear';

		result.status = 'Ясно';
	}

	return result;
}

// Refresh widget contents on the idle display:
function idleRefresh (on_timer) {
	var labels, fno, f, nt = false;

	if (util.isDef(on_timer) && on_timer)
	{
		fno = getForecastToDisplay();
		labels = getUpdatedLabels(fno);
	}
	else {
		forecast_display = 0;
		fno = forecast_display++;
		labels = getUpdatedLabels(fno);
	}

	f = weather_data.forecasts[fno];

	if (weather_data.tod == 0 && fno == 0)
		nt = true;
	else if (weather_data.tod == 1 && fno == 3)
		nt = true;
	else if (weather_data.tod == 2 && fno == 2)
		nt = true;
	else if (weather_data.tod == 3 && fno == 1)
		nt = true;

	var phen = getPhenomenaObj(f, nt);

	wnd[0] = new Image('app', phen.icon + '.gif', 15, Text.LINE_HEIGHT, 50, 50);
	wnd[2].label = labels.temp;
	wnd[3].label = phen.status;
	wnd[4].label = labels.press;
	wnd[5].label = labels.hum;
	wnd[6].label = labels.wind;

	clearTimeout(timeout_refresh);

	timeout_refresh = setTimeout(function(){
		idleRefresh(true);
	}, config['FORECAST_SCREEN_INT']);
}

// Function to finalize "get weather request"
function getWeatherCb (data) {
	if (data.hasOwnProperty('error') && data.error) {
		wnd[1].label = 'Обновление данных...';
		setTimeout(updateWeatherData, DEF_FORECAST_ERROR_UPDATE_INT);
	}
	else {
		weather_data = data; // overwrite previous data

		wnd[1].label = data.city + ', ' + week_days[data.weekday] +
			' ' + data.day + '.' + data.month;

		idleRefresh();
	}
}

// Get weather data from Gismeteo:
function getWeather (cb) {
	wnd[1].label = 'Обновление данных...';

	var uri = 'http://informer.gismeteo.ru/xml/' + config['CITY_ID'] + '.xml';

	var request = new NetRequest();

	request.open('GET', uri, true);

	request.onreadystatechange = function () {
		if (4 == request.readyState) {
			if (200 == request.status)
				cb(parseWeatherData(request.responseText));
			else {
				setTimeout(updateWeatherData, DEF_FORECAST_ERROR_UPDATE_INT);
			}
		}
	};

	request.send();

	clearTimeout(timeout_request);

	timeout_request = setTimeout(
		updateWeatherData,
		config['FORECAST_UPDATE_INT']
	);
}

// Function for usage in setTimeout func:
function updateWeatherData () {
	getWeather(getWeatherCb);
}

// Idle window initialization and reference store;
function initialize () {
	var cursor = 0, label;

	wnd = digium.app.idleWindow;

	wnd.add(new Image('app', 'unknown.gif', 15, Text.LINE_HEIGHT, 50, 50));

	for (var i = 0; i < 6; i++) {
		if (0 == i)
			label = new Text(0, Text.LINE_HEIGHT * cursor++, wnd.w, Text.LINE_HEIGHT, 'Получение данных...');
		else if (2 == i)
			label = new Text(10, 50, 65, Text.LINE_HEIGHT, 'Нет данных');
		else
			label = new Text(65, Text.LINE_HEIGHT * cursor++, wnd.w - 65, Text.LINE_HEIGHT);

		label.align(Widget.CENTER);

		wnd.add(label);
	}

	digium.app.exitAfterBackground = false;

	wnd.hideBottomBar = true;

	digium.event.observe({
		'eventName'	: 'digium.app.start',
		'callback'	: function () {
			setTimeout(function(){instantiated = true;}, 1000);
		}
	});

	digium.event.observe({
		'eventName'	: 'digium.app.idle_screen_show',
		'callback'	: function () {
			if (digium.app.idleWindowShown)
				idleRefresh();
		}
	});

	digium.event.observe({
		'eventName'	: 'digium.app.foreground',
		'callback'	: function () {
			// Stopping recursive call when error message box
			// should be shown by calling digium.foreground()
			if (onforeground_called) return ;

			onforeground_called = true;

			if (!instantiated) {
				// bring app to idle on the first launch:
				digium.background();
				instantiated = true;
				endForegroundMonitor();
			}
			else {
				// Show select options list: 1. setting up; 2. exit widget
				// showFormCities();
				showAppWindow();
			}
		}
	});
}

function showAppWindow () {
	if (!onforeground_called) return;

	digium.event.stopObserving({'eventName'	: 'digium.app.background'});

	digium.event.observe({
		'eventName'	: 'digium.app.background',
		'callback'	: function () {
			endForegroundMonitor();
			digium.event.stopObserving({'eventName'	: 'digium.app.background'});
		}
	});

	screen.clear();

	window.add(screen.setTitleText({'title' : 'Погода'}));

	try {
		window.add(new Text(4, 20, window.w, Text.LINE_HEIGHT, 'Выберите опцию:'));

		var menuObj = new List(4, 20 + Text.LINE_HEIGHT, window.w, window.h);

		menuObj.setProp('cols', 2).setProp('rows', 2);

		menuObj.set(0, 0, '1.');
		menuObj.set(0, 1, 'Выбрать город');
		menuObj.set(1, 0, '2.');
		menuObj.set(1, 1, 'Закрыть виджет');

		menuObj.setColumnWidths(15, 0);
		menuObj.select(0);

		menuObj.onFocus = function() {return true; };

		var selected = function() {
			if (0 == menuObj.selected)
				showFormCities();
			else {
				digium.app.exitAfterBackground = true;
				digium.background();
			}
		};

		menuObj.onkey1 = function(){ menuObj.select(0); selected(); };
		menuObj.onkey2 = function(){ menuObj.select(1); selected(); };

		menuObj.onkeyselect = selected;

		menuObj.setSoftkey(1, 'OK', selected);

		window.add(menuObj);

		menuObj.takeFocus();
	}
	catch(e) {
		window.add(new Text(0, 20, window.w, Text.LINE_HEIGHT, e.message));
	}
}

// Get summary configuration data with user-level priority:
config = util.defaults(getLocalConfig(), getApplicationConfig());

// Initialize:
initialize();

// Start app main cycle:
updateWeatherData();

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

Выглядит так

var cities = {"20476":"Mys Sterligova","20674":"Dikson","20744":"Malye Karmakuly","20864":"Tambey","20891":"Khatanga","20978":"Karaul","20982":"Volochanka","21802":"Saskylakh","21824":"Tiksi","21946":"Chokurdakh","22004":"Nikel","22019":"Polyarnyy","22028":"Teriberka","22112":"Kola","22113":"Murmansk","22127":"Lovozero","22204":"Kovdor","22213":"Apatity","22216":"Kirovsk (Murm.)","22217":"Kandalaksha","22324":"Umba","22408":"Kalevala","22429":"Solovki","22471":"Mezen","22522":"Kem-Port","22546":"Severodvinsk","22550":"Arkhangelsk","22559":"Kholmogory","22563":"Pinega","22573":"Leshukonskoe","22621":"Segezha","22641":"Onega","22671":"Karpogory","22686":"Vendinga","22695":"Koslan","22717":"Suoyarvi","22721":"Medvezhegorsk","22727":"Kondopoga","22762":"Dvinskoy Bereznik","22768":"Shenkursk","22778":"Verkhnyaya Toyma","22798":"Yarensk","22802":"Sortavala","22807"

и тд и тп.

Если коротко, то можно охарактеризовать разработку следующим образом:

1. Плюсы.
1.1 Javascript)
1.2 Наличие для взаимодействия с вебом объекта Netrequest (аля XHR);
1.3 Множество дополнителных объектов для выполнения рутинных задач и взаимодействия с предлагаемым API;
1.4 Иерархия предлагаемых к использованию встроенных объектов;
1.5 Механизм событий;
1.6 Поддержка отладки;

2. Минусы.
2.1 Небольшое количество примеров по использованию API вкупе с минималистичным подходом к самой документации;
2.2 Ограниченность в ресурсах — про парсинг большого кол-ва данных при помощи DomParser лучше даже не начинать думать, для обращения к небольшим XML вполне годен;
2.3 Нестандартные названия для некоторых объектов и элементов управления (например, List, который, вообще говоря, может отображать таблицу), что вместе с п. 2.1 делает «знакомство» с API чуть более долгим;
2.4 Нет встроенной «прокрутки» текста по таймеру при отображении в ограниченных для объёма текста размерах (пригодилось бы хотя бы в виджетах);
2.5 Иерархия доступных объектов нечёткая, некоторые вещи излишни (в целом, идея не плоха, но рассматривать как плюс можно только если у разрабов есть план по развитию всего этого);

3. Особенности.
3.1 Наличие объектов как window, так и screen (при этом window на самом деле является виджетом Group, не кисло, правда? — см. пп. 2.5, 2.3);
3.2 При разработке приложений для работы с веб нужно учитывать, что куки ответов для использования в javascript нельзя получить.

Автор: grigly

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js