Reference14r1:Concept App Service TechAssist

From innovaphone wiki
Jump to navigation Jump to search

Applies To

  • innovaphone PBX from version 13r2


The idea of the TechAssist app is to provide helpful tools for technicians, as well as to analyze static and runtime information of a system and check for problems and improvements. In this way, the app can help you find and solve problems and misconfigurations independently and quickly. For this purpose, the app supports

  • the execution of tests
    • a bunch of manufacturer tests are supplied with the app
    • you can create and save your own tests
    • testresults can be downloaded to attach them to a support ticket
  • a PBX license overview


There are no licenses needed for this app


Since V14

The TechAssist App is now installed by default for every new installation and is accessible from the Config Admin template.

With the PBX manager AP app installer plug-in

Ap app installer.png

Go to the PBX manager and open the "AP app installer" plugin. On the right panel, the App Store will be shown. Hint : if you access it for the first time, you will need to accept the "Terms of Use of the innovaphone App Store"

  • In the search field located on the top right corner of the store, search for "Techassist" and click on it
  • Select the proper firmware version, here "v14" and click on install
  • Tick "I accept the terms of use" and continue by clicking on the install yellow button
  • Wait until the install has been finished
  • Close and open the PBX manager to refresh the list of the available colored AP plugin
  • Click on the "AP techassist" and click on " + Add an App"
  • Enter a "Name" that is used as display name (all character allowed) for it and the "SIP" name that is the administrative field (no space, no capital letters). e.g : Name: TechAssist, SIP: techassist

Hint : The App needs access-right to one devices-api. The access to "devices-api" will be set automatically. If you want to have another devices-apis working with techassist, you have to configure it manually in the advanced UI.

  • Tick the appropriate template to distribute the App
  • Click OK to save the settings and a green check mark will be shown to inform you that the configuration is good

Test Engine - Overview

Technical look inside

Technical Overview

The app service receives multiple incoming websocket connections of the app objects from the PBXes. By assigning rights to a to your desired "devices-apis" (in the Apps tab), the app service receives the corresponding rights to connect to the respectively activated DevicesApps. All managed domains and devices can then be read in via this connection. (One TechAssist can only connect with one device-api) With this procedure, you can work with one DevicesApp instance and across all domains and devices in it.


The app contains various sub-modules in order to be able to carry out the tests provided by the manufacturer and also to be able to integrate custom tests created by yourself.

The scheduler initially loads all tests and performs a plausibility check of the individual tests. From this status onwards, the scheduler ensures that all tests are executed in the required time intervals.
Inside the Scheduler the tick is 5000 milliseconds, and test are executed sequentially to prevent high system load
The cmd submodule (derived from "command") can communicate with devices in the installation to query a status, the config or similar. The submodule takes care of all the information that tests from an installation want to have, consolidates them so that queries do not have to be executed multiple times and caches the results.
The test submodule is instantiated per test and provides all necessary functionalities within a test.


By design, only the app itself is available in multiple languages. The content of the tests and technical output is intentionally only available in english.


The app contains a bunch of embedded test, but it is also possible to create and save your own tests.


Expert Mode

By default there are some expert information hidden in the left menu bar. You will find a switch for that in the left menu (non-mobile-view) to enable all menu items.

Dealing with the Code Editor

The Code Editor is a tool for viewing the tests supplied and writing your own tests based on these tests.

The editor is divided into 3 basic areas.

Left - Code Editor
This is the code editor where you can write your test code in the browser. When you close the app, the content in the editor is saved, so that the next time you open the app you will find your old code again.
Top right - Menu bar
Here you will find the following functions:
Selection of all existing Tests (predefined vendor Tests and your own custom Tests)
Load the source code of the selected Test to the code editor
Delete the selected test from the app (This is only possible for custom tests)
Execute the source code (with or without cached commands in the service) in the Code Editor and show the result
Clear the Code Editor
Save the current Content from the code Editor as Test
The title of your test is divided from the title property in your test (lower based without non-alphanumeric characters). This means: If you save another Test with the same title the value will be saved in the other test.
Top right - Results
If you execute, save or delete a test you will get the response/results in this area

Create your own Testscript

You can create your own Test scripts in a JavaScript based environment. A test itself is a JavaScript object with some configurations properties and a section of freestyle code to use JavaScript habits to perform your test. If your test will be executed, an above layer will call your test() method and want to have a return object with your result.

There already exists a global Object called tests_add. You can add your own Test as Property to this existing Object like:

tests_add.my_new_test = tests_add.my_new_test || {
   title: 'Check for xyz',
   // configuration properties
   test: function (cmd, result) {
       // your custom test code

       return {
           success: false,
           msg: '....'

The way to add a test with tests_add.my_new_test = tests_add.my_new_test || {} is important so that two tests do not overwrite each other. Please also pay attention to this. In this example, we have a logical condition in the first line. If the test my_new_test already exists in the object tests_add the left side from the OR-condition is true, and the existing test takes place. Otherwise, it is false and the right side (your new test object) will be written to tests_add.my_new_test.

Configuration Properties

You must configure certain properties in your test so that the scheduler knows exactly how to handle your test.


tests_add.my_new_test = tests_add.my_new_test || {
  title: 'Unknown / New devices',
  description: 'Check for unassigned devices in your installation',
  todo: 'Please check the unknown devices in your installation and acceppt or delete the devices',
  author: '',
  enabled: true,
  schedule: '1h',
  commands: { ... },

  test: function (cmd, result) {
    // your custom test code

Mandatory properties

string | The title of your test, which is displayed in the UI
string | A short description of your test, which is displayed in the UI
string | An instruction on what the user has to do if the check fails. It will be displayed above the logfile in failed tests. (You can use \r\n for line breaks)
string | The contact of the person who create the test
string | The time interval during which the test is to be carried out independently. The format is composed of a numerical value and interval value. This allows you to create combinations such as 5m (every 5 minutes), 10h (every 10 hours) or 1d (every day) . A special case which is also allowed is the value none. With a none value, your test will not be executed automatically, but is available in the app. So the user is able to execute your test manually.
Possible interval values are:
s (seconds)
m (minutes)
h (hours)
d (days)
w (weeks)
Please note that each execution of a test requires resources. For example, if you create a test with 1m that is executed every minute, you may well create an unpleasant impact! The system automatically checks the execution time of your test against your specified interval. If your test runs for a longer time than your schedule, your schedule parameter is automatically doubled by the system.
Please note the Submodule Scheduler with Tick and synchronous execution. For example, it is not possible to execute a test every second.

Optional properties

object | An object in which you can list all the commands, values, etc. that you want to use later in your test. As soon as your test() method is called, the returns of these commands are passed in as parameters so that you can analyze them. For detailed documentation, please see the commands-documentation below.
bool | If false the test will be loaded but not executed. The user will see the test in the list of test, and it is possible to enable it (default: true)
bool | If true the test will be skipped by the system and will not be loaded (default: false)

Property "commands"

The property commands is an object where you can define innovaphone specific commands that are executed before your test will be executed. The results of the given commands will be passed to your test in the parameter results. Think about that, all your commands will be cached until the schedule timeout is exceeded. This means that you will only get a new result of the command after the schedule time has elapsed in relation to the time of your last execution. The only exceptions is when the user manually re-executes a test via the UI or caching has been explicitly disabled at the command

You have to consider a special nested notation when creating your commands, so that you can use them later.

outer commands-object

Let's start with some first basic rules:

  • commands is an object with one property per command. At this moment we only list commands, we don't take care about specific devices, types of models or so on.
  • Every child-property (let's call it "command-object") is a new object which contains all needed information for this specific command
  • The title of the property "command-object" is defined by yourself
    • Spoiler: The title of your "command-object" should be used later if you want to read the result for a specific device
  • You can list multiple "command-objects"

Up to here we have the following and can define it like this:

commands: {
  my_command_one: {},
  my_command_two: {},

single command-object

Then we have some new rules per "command-object":

  • A property called cmd is expected. This property contains the real command which shall be executed (Remember, at this moment we only list commands, we don't take care about specific devices, types of models or so on)
    • Example 1: cmd: 'CMD0/box_info.xml' will give you the xml output of the general overview page
    • Example 2: cmd: '!mod cmd CDR0 qs' will give you the amount of buffered CDRs in the CDR0 interface
    • Example 3: cmd: 'cfg.txt' will give you back the full configuration file
    • If you are already firm with the innovaphone based command syntax, you will see that you can use any HTTP command which can be sent to an innovaphone device
  • Some more properties can be defined optionally. See documentation below

Up to here we have an idea of a specific "command-object" and can do the following now:

commands: {
  buffered_cdr0: {
    cmd: '!mod cmd CDR0 qs',
  buffered_cdr1: {
    cmd: '!mod cmd CDR1 qs',

With this command, we can collect the "buffered CDRs" on both CDR Interfaces in our boxes. Maybe you see it directly... This will not make really sense in a real setup because CDRs only generated by Gateways/PBXes. So we don't need to bother phones with these requests. So from now on we take care about our defined command. There are optional parameters that we can also list inside a "command-object" to let the system know what is the to-do with this command.

string | Any HTTP command you can send to an innovaphone device. There is a limit of 15 seconds for every http request. If your request exceeds this limit the request will be cancelled.
array | An optional list of device types on which the command is to be executed. If cat is empty, it means all devices in your installation.
You can define the following device categories:
pbx - all PBXes
gw - all Gateways
ap - all App Platforms
dect - all Dect Gateways
phone - all Phones
bool | If you set it to false you will never get a cached value. Please use this option wisely and only if you really need it. (default: true)

Now let's improve our example from above and get the bufferd CDRx information only from pbxes and gateways.

commands: {
  buffered_cdr0: {
    cmd: '!mod cmd CDR0 qs',
    cat: ['pbx', 'gw'],
  buffered_cdr1: {
    cmd: '!mod cmd CDR1 qs',
    cat: ['pbx', 'gw'],

execution and results of commands

Up to now, we have only defined commands that are to be executed on certain devices. When your test is executed, the previously defined commands are executed and the system collects the returns in a corresponding object (called results) which is passed into your test(cmd, results) method. There you can access the results and do whatever you want to test/analyze or whatever with the return value. You will find an example and some sample-code how to use the results-object in the description of the test() method

nested commands-objects

You can now send any commands and the results will be returned to you for evaluation. As you may have already noticed, there is no possibility to place multiple commands in dependency. But this is the way to do this.

For more complicated tests, it is necessary to execute several commands one after the other and to generate following commands dynamically on the basis of the result of the first command.

For such tests, there is the possibility to specify an object "commands" within a "command-object". From this level, you can define a dynamic function in which you can apply your own code based on the first result, and then return a new commands-object. In this return statement, you also have the option of returning another dynamic function in order to be able to create processes of any complexity. Please note that if you not return an object your return value will be discarded. The results of your nested command-objects will be passed to the test() method inside the results object.

This is certainly not easy to understand. Therefore, let us take a closer look at it with an example.

example: nested commands-object which generates the second command from the first result
commands: {
  certificates: {
    cmd: 'X509/mod_cmd.xml',
    cat: ['pbx', 'gw', 'dect'],
    commands: function(cmd, result) {
                // free style code to build the following commands
                var xml = cmd.parse_xml(result);
                var fingerprint;
                // get the fingerprint for single cert
                if (typeof['@fingerprint'] !== 'undefined') {
                    fingerprint =['@fingerprint']
                // use last certificates in case of multiple
                else {
                    fingerprint =[ - 1]['@fingerprint'];
                cmd.log('Fingerprint: '+fingerprint);
                return {
                    certificate: {
                        cmd: 'X509/mod_cmd.xml?cmd=servercert-details&fingerprint='+fingerprint,

Our goal is to check which key-length is used in the device certificate. But there i no direct command to get this information. So we have to do it in multipe steps.

In this sample, we request the following in the commands object:

  1. Execute our command "certificates" on devices (pbx, gw, dect) the command "X509/mod_cmd.xml" which will give you back the xml payload of the page Reference14r1:General/Certificates
  2. If the first request is done, the nested "commands" object should be called and we get the result from the first command
    1. Now, inside the function commands: function(cmd, result) { we can use some free-style code to to what we need, to build a following command
    2. Here we convert xml to an object and extract the certificate fingerprint from the result of the first request
    3. then we return an object with "command-objects"
      1. think about that is possible to add an new commands: function(cmd, result) { inside the return to add a new level :-)
    4. this second command will be executed to for every device and the xml payload contains the device certifcate of the current device. (like the popup in the UI if you click on a specific certificate)
  3. If all nested commands are processed, then your test() will be called, and you get all the results (in this case "certificates" and "certificate") in the results-object

example: nested commands-object which generates various commands from the first result
commands: {
	config: {
		cmd: 'cfg.txt',
		cat: ['pbx'],
		commands: function(cmd, result) {
			var lines = result.split("\r\n");
			var output = {};
			// get pbx loc
			var loc = false;
			for (var l = 0; l < lines.length; l++) {
				if (!lines[l].startsWith('config change PBX0')) continue; // skip non pbx line
				if (lines[l].includes('/mode standby')) continue; // skip slave pbxes
				var match = lines[l].match(/config change PBX0 (.*) \/loc ([^ ][^/]+)/);
				loc = match[2].substr(0, match[2].length-1);
				l = lines.length;
			cmd.log('use pbx loc: '+loc);
			// get user objects
			var regex = new RegExp('/\(loc='+loc+'\)(.*)\(guid;bin=([0-9A-Za-z]+)\)/');
			for (var l = 0; l < lines.length; l++) {
				// skip if user is not from this pbx
				if (!lines[l].includes('(loc='+loc+')')) continue;
				var match = lines[l].match(/\(guid;bin=([0-9A-Za-z]+)\)/);
				if (!match) continue;
				cmd.log('user found: '+match[1]);
				output[match[1]] = {cmd: 'PBX0/ADMIN/mod_cmd_login.xml?cmd=show&user-guid='+match[1]};
			return output;

Our goal is to get the xml output from the advanced ui for every pbx object. But there i no direct command to get this information. So we have to do it in multipe steps.

In this sample, we request the following in the commands object:

  1. Execute our command "config" on PBXes the get the plain configuration file
  2. If the first request is done, the nested "commands" object should be called, and we get the result from the first command
    1. Now, inside the function commands: function(cmd, result) { we can use some free-style code to do what we need, to build multiple following commands
    1. Here we split the config file by lines breaks and iterate the whole config
    2. then we generate an empty return object to fill it later
      1. think about that is possible to add an new commands: function(cmd, result) { inside the return to add a new level :-)
    3. If we have checked the pbx and the userobject we add with output[match[1]] = {cmd: 'PBX0/ADMIN/mod_cmd_login.xml?cmd=show&user-guid='+match[1]}; a specific command to the retunr value

In the end, we have a big return object with multiple cmd's In this case we will get all the results in the test() later and can do further stuff with the results.

Executable method test()

The upper layer expects a method test(cmd, results) in your JavaScript object. This method has two parameters.

An object that offers you a wide range of predefined functions and options. You can see it as a kind of library, which is documented at the cmd object.
An object of the results of your commands from the given command-objects in your test definition.

return test result

Your test has to be return an object with your test result. The system expect an object with the following properties.

boolean | indicates if your test was successful or not
string | an optional short message that will be displayed in the UI as preview
array | an optional array with lines of logfile that you can use to display detailed information in the UI at your test result. Use log.push() to fill up the logfile array. In the frontend, the log will be parsed and mac addresses /(^|\W)([a-f0-9]{12})(\W|$)/i will be updated with an internal link to devices app. (If you want to create instead debug output to the app-instance logfile you can use cmd.log())
test: function (cmd, results) {
  // your stuff
  var count = 2;
  return {
    success: false,
    msg: 'There are '+count+' new/unassigned devices in your setup'

The full picture

Complete sample test to check buffered CDRs

tests_add.cdrs_buffered = tests_add.cdrs_buffered || {
    title: 'Buffered CDRs',
    description: 'This test will check if CDRs on a gateway was buffered and are not transmit to the CDR target yet',
    todo: 'Check the CDR target',
    author: '',
    enabled: true,
    schedule: '10m',
    commands: {
        cdr0: {
            cmd: '!mod cmd CDR0 qs',
            cat: ['gw'],
        cdr1: {
            cmd: '!mod cmd CDR1 qs',
            cat: ['gw'],
        cdr2: {
            cmd: '!mod cmd CDR2 qs',
            cat: ['gw'],
        cdr3: {
            cmd: '!mod cmd CDR3 qs',
            cat: ['gw'],
        cdr4: {
            cmd: '!mod cmd CDR4 qs',
            cat: ['gw'],
    test: function (cmd, results) {
        var log = [];
        var success = true;
        for (var n = 0; n < 5; n++) {
                var check = calculate_cdrs(results['cdr'+n][i]);
                // result was not good, lets create a human error message
                if (check !== true) {
                    log.push(cmd.get_device(i).product+' / '+cmd.get_device(i).hwId+' / '+check+'% - '+results['cdr'+n][i]);
                    if (success) success = false;
        return {
            success: success,
            msg: success ? '' : 'Devices with buffered CDRs exists',
            log: log
        function calculate_cdrs(str) {
            // QS messages: 0 max 2000 chars: 0 max 300000
            var check = 0;
            str = str.split(' ');
            // log.push(str);
            var messages_current = str[2];
            var messages_max = str[4];
            var chars_current = str[6];
            var chars_max = str[8];
            // check messages
            check = cmd.percent(messages_current, messages_max);
            if (check >= 30) return check;
            // check chars
            check = cmd.percent(chars_current, chars_max);
            if (check >= 30) return check;
            return true;

Complete sample test to check firmware versions

tests_add.firmware_version = tests_add.firmware_version || {
    title: 'Supported firmware versions',
    description: 'Check if you have unsupported firmware version in use',
    todo: 'Update the firmware on affected devices',
    author: '',
    schedule: '1d',
    commands: {
        box_info: {
            cmd: 'CMD0/box_info.xml',
    test: function (cmd, results) {
        var log = [];
        var success = true;
        var msg = '';
            var xml = cmd.parse_xml(results.box_info[i]);
            if (typeof === 'undefined') return;
            if (typeof === 'undefined') return;
            if (typeof['@version'] === 'undefined') return;
            if (
      ['@version'].includes('6.00') ||
      ['@version'].includes('7.00') ||
      ['@version'].includes('8.00') ||
      ['@version'].includes('9.00') ||
      ['@version'].includes('10.00') ||
      ['@version'].includes('11.00') ||
      ['@version'].includes('11r1') ||
      ['@version'].includes('12r1') ||
            ) {
                log.push(cmd.get_device(i).product+' / '+cmd.get_device(i).hwId+' / '['@version']);
                if (success) success = false;
        return {
            success: success,
            msg: success ? '' : 'You use old firmware which is no longer be supported',
            log: log

cmd object

The cmd object provides you the following methods:

get device information / system information

get_domains() : object

You will receive all existing domains

get_devices() : object

You will receive all existing devices

get_devices_from_domain(int id) : object

You will receive all devices which are assigned to the given domain

get_device(int id) : object

You will receive a specific device

helper functions

percent(int value, int total) : float

Returns the number of percent of value to total

parse_xml(string xml, string element = '') : object

Returns an object/array representation of given XML string. Note that very large XML inputs cannot be converted directly because there are some restrictions in the duktape environment. If you receive an exception with Error: RangeError: regexp executor recursion limit you have to define which element do you want to converted (See the second example).

is_supported_version(string version) : object

Return if the given Version string is supported according to Howto:Supported innovaphone versions.


log(string message) : void

You can write a logfile line to the app debug log

Secret knowledge for good tests

bool PBX-Object config attributes
You will find config attributes like no-subscribe="true" (this is from a Trunk Object) in the config. Please note that if the attribute is missing or the value is false it is false, otherwise it is true. For example, the value no-subscribe="on" is also true.
bool Interface config attributes
You will find config attributes like /direct-sig on (this is from a SIP interface) in the config. Please note that they are exists bool-attribute which are true if the attribute is existing. In the case of bool-attribute the following value make no matter. For example, the attribute /direct-sig off is also true.


Startup Logfile

If you enable the traceflag App in your App instance, you will find the following logs:

App instance started 14C1363 14C1363
// App begins to load embedded tests
JavaScript: Files tests/*
// Scheduler begins to load all the tests
scheduler init
init test "firmware_version"
firmware_version: Test added successful
init test "pbx_mediarelay"
pbx_mediarelay: Test added successful