cancel
Showing results for 
Search instead for 
Did you mean: 
cancel

xAPI over WebSocket - XoWS (CE9.7.x)

1971
Views
15
Helpful
2
Comments
Cisco Employee

It´s time for a new software release CE9.7.1 has just arrived, introducing an interesting feature called XoWS (pronounced cows)

This feature basically allows you to setup a persistent bi-directional connection to the xAPI. WebSocket is just an alternative way of communicating with the xAPI like SSH, Serial or XMLAPI, but using JSON RPC 2.0 as data transport, you send and receive all the data in JSON format which is easier to work with than parsing XML or scraping plain text.

Most of the popular programming languages have a WebSocket module you can use to integrate your app/script with XoWS.

The difference between a HTTP request and a WebSocket is that a HTTP request is client initiated and a response is received for the request before the connection is closed. To send and receive more data you have to generate new client requests.

With a WebSocket the communication channel is open both ways and the server can send data to the client when new data is available.

More information can be found here:

https://en.wikipedia.org/wiki/WebSocket

In order to get started and open the device for connections you need to enable the WebSocket service on the device.

xConfiguration NetworkServices Websocket: FollowHTTPService

We set the Websocket config to FollowHTTPService. The connection is setup via HTTP so if you disable the HTTP service on the device, Websocket will not work.

The WebSocket entrypoints is:

ws(s)://<codecip>/ws
ws(s)://<codecip>/ws?legacy

These are the URLs you use when setting up the connection /ws and /ws?legacy is essentially the same but /ws is integer aware which means switching your jsxapi app from SSH directly to using WebSocket will most likely break something in your existing code. Legacy is setup to avoid that and returns all values as strings. This is the difference between the two. 

Feature summary


Benefits

  • Simple mapping between request and response, no difference between "sync" and "async" responses.
  • Each message contains a complete JSON document and nothing else, no need to parse text or a mix of text and XML.
  • Can be accessed directly from a browser using javascript (using auth protocol header)(Currently not supported)

Benefits with respect to HTTP API (I.E XMLAPI)

  • Much faster. No need to re-authenticate for every request.
  • Clients can get events from the codec. No need to run your own web server and register for HttpFeedback or listen for HTTP POSTS and PUT from the codec.

Benefits with respect to XAPI over SSH/telnet/RS-232

  • The SSH/telnet/RS-232 services can be turned off if not used for anything else.

Feature description, functional overview, compatibility


Purpose

Provide a modern an powerful way of interacting with the xAPI using WebSocket. Keep in mind that this is an integration feature which allows flexibility on how you choose to use the API. You can write an application that uses WebSocket to monitor devices or whatever other use-case you can think of.

XoWS - Guide

Connecting

Basic Authentication: HTTP request header
HTTP request header “Authorization: Basic <encoded user:pw>” before upgrading the HTTP connection to a WebSocket. User needs to provide a valid user:password combination using basic auth.

Auth protocol header (Disabled in CE9.7.1, planned for a future release)

Because browser based clients have no direct control over headers, you can also authenticate using an auth protocol header. The authentication is sent similarly to basic authentication, i.e. as a base64 encoded string of username:password. The complete header looks like this: Sec-WebSocket-Protocol: auth-dXNlcjpwYXNz. Example usage from javascript (the replace train is for making sure the base64 is "url-safe" else it will not always work):

var ws = new WebSocket("wss://codec.example.com/ws", "auth-" + btoa("user:password").replace(/[\/+=]/g, function(c){return {'+':'-','/':'_','=':''}[c]}));

Possible error codes
Currently, these are the possible error codes:
(from the JSON RPC spec):
        Invalid Request, -32600
        Method Not Found, -32601
        Invalid Params, -32602
        Internal Error, -32603
        Parse Error, -32700
(implementation-defined server errors):
        Unexpected response, -32099
(application-defined server errors):
        CommandError, 1
        Permission Denied, -31999
        Subscriber Count Exceeded, -31998
        Not Ready, -31997

Let´s take a look at the JSON documents that we can send and receive to the codec.

In the JSON document we have some required fields, jsonrpc, id, method and params.

jsonrpc needs to be set to 2.0 since that is the version we are using.

id can be set to anything you like in order to map the request with the response, the response will contain the same id as the request.

method is basically the "xcommand/xconfig" but its a little bit different, since with WebSocket we use xGet, xSet, xQuery, xCommand/path/to/basic etc.. but we will get back to that as we move on.

params is all the parameters to the method, it could be parameters or a path

To get started you need to choose your cup of language that you wish to use and find out if it has a websocket library. I will be using mostly Python3.7 in my examples and at the end I will show you how to start using WebSocket with jsxapi and NodeJS.

I also assume you have some experience of python and how to setup your environment. 

Before you read further: Note that the following examples are not using the newly released XoWS library (pyxows for Python 3.7). If you scroll further down you will find examples that uses the pyxows library. I strongly recommend you to use the pyxows library but I will keep the below examples in this article for learning purposes.

For Python3.7 you can use a WebSocket library (I am using the websockets module) which can be acquired by issuing

 

pip install websockets

 

 

Below is a basic script I will use to show you how it works, as a start.

xows-entry.py

import websockets
import ssl
import asyncio
import base64

async def connect():
return await websockets.connect('wss://{}/ws/'.format('10.10.10.1'), ssl=ssl._create_unverified_context(), extra_headers={
'Authorization': 'Basic {}'.format(base64.b64encode('{}:{}'.format('admin', '').encode()).decode('utf-8'))})

async def send(ws, message):
await ws.send(message)
print('Sending:', message)

async def receive(ws):
result = await ws.recv()
print('Receive:', result)

async def task():
ws = await connect()
try:
await send(ws, '{"jsonrpc": "2.0","id": "0","method": "xGet","params": {"Path": ["Status", "SystemUnit", "State"]}}')
await receive(ws)
finally:
ws.close()

asyncio.run(task())

This simple example connects to the XoWS, send a payload through the socket and receive data back before the script ends. See below for the output.

Sending:  {"jsonrpc": "2.0","id":"0","method": "xGet","params": {"Path": ["Status", "SystemUnit", "State"]}}
Receiving: {"jsonrpc":"2.0","id":"0","result":{"NumberOfActiveCalls":0,"NumberOfInProgressCalls":0,"NumberOfSuspendedCalls":0}}

So the connection works, we can send data and receive data. In a WebSocket connection we dont deal with posts / puts / gets we send data and receive data back or we just receive data (feedback events, for example).

 

Here is a modified version of the above to make sending data a little bit easier, as the functions create the json documents automatically and I can interact with the xAPI by writing much simpler commands if you look in the "task" function now I use the get function to generate the JSON document while keeping the syntax almost the same as I would type it in the xAPI CLI.

import websockets
import ssl
import asyncio
import base64
import json

count = 0

async def connect():
return await websockets.connect('wss://{}/ws/'.format('10.10.10.1'), ssl=ssl._create_unverified_context(), extra_headers={
'Authorization': 'Basic {}'.format(base64.b64encode('{}:{}'.format('admin', '').encode()).decode('utf-8'))})

def construct(method):
global count
count += 1
return {'jsonrpc': '2.0', 'id': str(count), 'method': method}

def query(params):
payload = construct('xQuery')
payload['params'] = {'Query': params.split()}
return payload

def get(params):
payload = construct('xGet')
params = [i if not i.isnumeric() else int(i) for i in params.split()]
payload['params'] = {'Path': params}
return payload

def command(path, params=None):
payload = construct('{}{}'.format('xCommand/', '/'.join(path.split(' '))))

# Params are for multiline commands and other command parameters {'ConfigId':'example', 'body':'<Extensions><Version>1.0</Version>...</Extensions>'}
if params != None:
payload['params'] = params

return payload

def config(path, value):
payload = construct('xSet')
payload['params'] = {
"Path": ['Configuration'] + path.split(' '),
"Value": value
}
return payload

def feedbackSubscribe(path=None, notify=False):
payload = construct('xFeedback/Subscribe')
payload['params'] = {
"Query": path.split(' '),
"NotifyCurrentValue": notify
}
return payload

async def send(ws, message):
await ws.send(json.dumps(message))
print('Sending:', message)

async def receive(ws):
result = await ws.recv()
print('Receive:', result)

async def task():
ws = await connect()
try:
await send(ws, get('Status SystemUnit Uptime'))
await receive(ws)
finally:
ws.close()

asyncio.run(task())

Here is the result of the run

Sending: {'jsonrpc': '2.0', 'id': '1', 'method': 'xGet', 'params': {'Path': ['Status', 'SystemUnit', 'State']}}
Receive: {"jsonrpc":"2.0","id":"1","result":{"NumberOfActiveCalls":0,"NumberOfInProgressCalls":0,"NumberOfSuspendedCalls":0}}

So pretty much the same output. Let´s go through the guide and put it to the test.

Status and configuration
Get a single leaf node.

Send:
{ "jsonrpc": "2.0", "id": "a1336", "method": "xGet", "params": { "Path": ["Status", "SystemUnit", "Uptime"] } }
Receive: { "jsonrpc": "2.0", "id": "a1336", "result": 62974 }

Lets see how this works with the script:

-snip-
await
send(ws, get('Status SystemUnit Uptime'))
-snip-
Sending: {'jsonrpc': '2.0', 'id': '1', 'method': 'xGet', 'params': {'Path': ['Status', 'SystemUnit', 'Uptime']}}
Receive: {"jsonrpc":"2.0","id":"1","result":64882}

Moving on..

Get a single subtree.

Send:
{ "jsonrpc": "2.0",
"id": "3" "method": "xGet", "params": { "Path": ["Status", "SystemUnit", "State"] } } Receive:

{ "jsonrpc": "2.0", "id": "3", "result": { "NumberOfActiveCalls": 0, "NumberOfInProgressCalls": 0, "NumberOfSuspendedCalls": 0 } }

We already tested this above, so skipping this one.

Single leaf nodes from inside arrays can be retrieved by specifying the one-based index with a Number in the path.

Send:
{ "jsonrpc": "2.0", "id": "a1338", "method": "xGet", "params": { "Path": ["Status", "SystemUnit", "Hardware", "Monitoring", "Fan", 2, "Status"] } }
Receive: { "jsonrpc": "2.0", "id": "a1338", "result": "2372 rpm" }

If you notice in the get function there is a check for numeric values:

params = [i if not i.isnumeric() else int(i) for i in params.split()]

and this will make sure that my path is built with an in when I send the string below.

await send(ws, get('Status SystemUnit Hardware Monitoring Fan 2 Status'))
Sending: {'jsonrpc': '2.0', 'id': '1', 'method': 'xGet', 'params': {'Path': ['Status', 'SystemUnit', 'Hardware', 'Monitoring', 'Fan', 2, 'Status']}}
Receive: {"jsonrpc":"2.0","id":"1","result":"2820 rpm"}

Query with wildcard in path

Works on both Status and Configuration. "**" matches zero or more levels in the path.

Send:
{ "jsonrpc": "2.0", "id": "a1350", "method": "xQuery", "params": { "Query": ["Status", "**", "DisplayName"] } }
Receive: { "jsonrpc": "2.0", "id": "a1350", "result": { "Status": { "SIP": { "CallForward": { "DisplayName": "" } }, "SystemUnit": { "Software": { "DisplayName": "ce 9.7.1 xxx" } } } } }

Ok, then we switch to the query function of the script:

await send(ws, query('Status ** DisplayName'))
Sending: {'jsonrpc': '2.0', 'id': '1', 'method': 'xQuery', 'params': {'Query': ['Status', '**', 'DisplayName']}}
Receive: {"jsonrpc":"2.0","id":"1","result":{"Status":{"SIP":{"CallForward":{"DisplayName":""}},"SystemUnit":{"Software":{"DisplayName":"ce 9.7.1 30bff6140aa 2019-04-02"}}}}}


Set configuration

Send:
{ "jsonrpc": "2.0", "id": "a1360", "method": "xSet", "params": { "Path": ["Configuration", "SystemUnit", "Name"], "Value": "my-codec" } }
Receive: { "jsonrpc": "2.0", "id": "a1360", "result": true }

Ok, I will run this through the config function of the script:

await send(ws, config('SystemUnit Name', 'my-codec'))
await send(ws, get('Configuration SystemUnit Name'))
Sending: {'jsonrpc': '2.0', 'id': '1', 'method': 'xSet', 'params': {'Path': ['Configuration', 'SystemUnit', 'Name'], 'Value': 'my-codec'}}
Receive: {"jsonrpc":"2.0","id":"1","result":true}
Sending: {'jsonrpc': '2.0', 'id': '2', 'method': 'xGet', 'params': {'Path': ['Configuration', 'SystemUnit', 'Name']}}
Receive: {"jsonrpc":"2.0","id":"2","result":"my-codec"}

Commands

Send:
{ "jsonrpc": "2.0", "id": "a1370", "method": "xCommand/Dial", "params": { "Number": "alice@example.com", "Protocol": "Spark" } }
Receive: { "jsonrpc": "2.0", "id": "a1370", "result": { "CallId": 2, "ConferenceId": 1 } }
Receive error: { "jsonrpc": "2.0", "id": "a1370", "error": { "code": -32601, "message": "Method not found." } }

Sending commands is a little different since the command path is in the method field, but the command function in the script will take care of building the JSON blob, all I need to type in is something that reminds me a lot of the macro command syntax:

await send(ws, command('CallHistory Get', {'Limit': '1'}))
Sending: {'jsonrpc': '2.0', 'id': '1', 'method': 'xCommand/CallHistory/Get', 'params': {'Limit': '1'}}
Receive: {"jsonrpc":"2.0","id":"1","result":{"status":"OK","Entry":[{"id":0,"CallHistoryId":2,"CallbackNumber":"sip:user@cisco.com","DisplayName":"user@cisco.com","StartTime":"2018-11-13T14:44:44","DaysAgo":141,"OccurrenceType":"NoAnswer","IsAcknowledged":"Acknowledged","RoomAnalytics":{}}],"ResultInfo":{"Offset":0,"Limit":1}}}

If the command does not add up you will get "Method not found".

Multi-line commands.

Multi-line commands are identical to regular commands, but they provide the command body in the "body" parameter. A Multi-line command is a command that takes text as input. For example if you want to upload In-Room Control design as pure XML via WebSocket instead of uploading as a file through the web interface. Please see the API Reference guides for more information about Multi-line commands.

Send:
{ "jsonrpc": "2.0", "id": "a1370", "method": "xCommand/UserInterface/Extensions/Set", "params": { "ConfigId": "example", "body": "<Extensions><Version>1.0</Version>...</Extensions>", } }

Ok, in my example I am just going to send a HTTP POST to a server since this is a multiline command.

payload = json.dumps({"field1":"value1"}) <- body payload
await send(ws, command('HttpClient Post', {'Url':'http://10.10.10.1:5000/api',
'Header':'Content-Type: application/json',
'body': payload})) <- multiline content here
Sending: {'jsonrpc': '2.0', 'id': '1', 'method': 'xCommand/HttpClient/Post', 'params': {'Url': 'http://10.10.10.1:5000/api', 'Header': 'Content-Type: application/json', 'body': '{"field1": "value1"}'}}
Receive: {"jsonrpc":"2.0","id":"1","result":{"status":"OK","StatusCode":200}}

Server receives (simple Flask driven API in python):

* Running on http://10.10.10.1:5000/ (Press CTRL+C to quit)
{'field1': 'value1'}
10.10.10.2 - - [09/Apr/2019 23:21:17] "POST /api HTTP/1.1" 200 

Status feedback and events
Register feedback.

"NotifyCurrentValue": When set to "true" there should be feedback notification triggered with the current value of the config/status nodes matching the expression.

"Query": Subscribing to all feedback can be done by subscribing to "Query": ["**"].

Send:
{ "jsonrpc": "2.0", "id": "a1400", "method": "xFeedback/Subscribe", "params": { "Query": ["Status", "Audio", "Volume"], "NotifyCurrentValue": false, } }
Receive: { "jsonrpc": "2.0", "id": "a1400", "result": { "Id": 3 } }

Feedback events is one of the more powerful things you can do with websockets without the need for a web server to receive the feedback events. The feedback events will be sent through the open connection from the codec when new feedback is available.

Lets do it!

await send(ws, feedbackSubscribe('Status Audio Volume', True))
while True:
await receive(ws)

I added a listener on this so the script does not complete. This is bad programming, I know but just to show you an example of how you can listen for feedback.

Sending: {'jsonrpc': '2.0', 'id': '1', 'method': 'xFeedback/Subscribe', 'params': {'Query': ['Status', 'Audio', 'Volume'], 'NotifyCurrentValue': True}}
Receive: {"jsonrpc":"2.0","id":"1","result":{"Id":0}}
Receive: {"jsonrpc":"2.0","method":"xFeedback/Event","params":{"Status":{"Audio":{"Volume":70}},"Id":0}}

There, we have registered a feedback listener on the Status value for Audio Volume. So whenever the Volume on the device changes we should receive the new value through our WebSocket channel.

Deregister feedback

Send:
{ "jsonrpc": "2.0", "id": "a1401", "method": "xFeedback/Unsubscribe", "params": { "Id": 3 } }
Receive: { "jsonrpc": "2.0", "id": "a1401", "result": true }
Receive error: { "jsonrpc": "2.0", "id": "a1401", "error": { "code": -32602, "message"; "Invalid params", "data": "Unknown feedback ID." } }

This is just deregistering the feedback based on the feedback ID you registered with.

Receiving feedback

Feedback is received as JSON-RPC notifications. The feedback message will include the feedback id. By changing the audio level we receive the following feedback message (after we registered the above feedback subscription):

Receive:
{
"jsonrpc": "2.0",
"method": "xFeedback/Event",
"params": {
"Status": {
"Audio": {
"Volume":60
}
},
"Id":3
}
}

I log into the device on SSH and execute:

xcommand Audio Volume Set Level: 80

OK
*r VolumeSetResult (status=OK):
** end

And a couple of other values, produces this as an output in my script:

Receive: {"jsonrpc":"2.0","method":"xFeedback/Event","params":{"Status":{"Audio":{"Volume":80}},"Id":0}}
Receive: {"jsonrpc":"2.0","method":"xFeedback/Event","params":{"Status":{"Audio":{"Volume":50}},"Id":0}}
Receive: {"jsonrpc":"2.0","method":"xFeedback/Event","params":{"Status":{"Audio":{"Volume":40}},"Id":0}}

Pretty cool!

Get complete documents
Both xGet and xQuery can get an entire tree / subtree. These need to be fetched separately, i.e. by passing ["Configuration"] or ["Status"]. ["**"] is not valid.

Lets just do an example in NodeJS:

In NodeJS it can look like this (requires NodeJS environment and WebSocket module) websocket.js - Working example to get you started on NodeJS:

const WebSocket = require('ws');
const url = 'wss://10.10.10.1/ws/'

const websoc = new WebSocket(url, {
  perMessageDeflate: false,
  headers: {
    Authorization: `Basic YWRtaW4xOg==`,
  },
  rejectUnauthorized: false,
});

websoc.onopen = function (event) {
  websoc.send('{"jsonrpc": "2.0","id": "a1336","method": "xGet","params": {"Path": ["Status", "SystemUnit", "Uptime"]}}');
  websoc.send('{"jsonrpc": "2.0","method": "xGet","params": {"Path": ["Status", "SystemUnit", "State"]}}');
  websoc.send('{"jsonrpc": "2.0","id": "a1400","method": "xFeedback/Subscribe","params": {"Query": ["Status", "Audio", "Microphones", "Mute"],"NotifyCurrentValue": true,}}');
}
websoc.onmessage = function (event) {
  console.log(event.data);
}

websoc.on('error', function(event) {
  console.log(event);
});

Output:

NodeJS mohm$ node websockets.js
{"jsonrpc":"2.0","id":"a1336","result":510806}
{"jsonrpc":"2.0","id":null,"result":{"NumberOfActiveCalls":0,"NumberOfInProgressCalls":0,"NumberOfSuspendedCalls":0}}
{"jsonrpc":"2.0","id":"a1400","result":{"Id":0}}
{"jsonrpc":"2.0","method":"xFeedback/Event","params":{"Status":{"Audio":{"Microphones":{"Mute":"Off"}}},"Id":0}}
{"jsonrpc":"2.0","method":"xFeedback/Event","params":{"Status":{"Audio":{"Microphones":{"Mute":"On"}}},"Id":0}}
{"jsonrpc":"2.0","method":"xFeedback/Event","params":{"Status":{"Audio":{"Microphones":{"Mute":"Off"}}},"Id":0}}
{"jsonrpc":"2.0","method":"xFeedback/Event","params":{"Status":{"Audio":{"Microphones":{"Mute":"On"}}},"Id":0}}
{"jsonrpc":"2.0","method":"xFeedback/Event","params":{"Status":{"Audio":{"Microphones":{"Mute":"Off"}}},"Id":0}}

If you are using jsxapi which is the NodeJS module for writing "external macros" you can switch from SSH to using WebSocket. See the following example (I know it seems cumbersome but this will improve going forward, finding easier ways of utlizing the WebSocket as a backend):

ws-jsaxpi.js

const WebSocket = require('ws');
const XAPI = require('jsxapi/lib/xapi').default;
const WSBackend = require('jsxapi/lib/backend/ws').default;

const url = 'ws://codecip/ws';
const username = 'admin';
const password = '';

const auth = Buffer.from(`${username}:${password}`).toString('base64');
const options = {
  headers: {
    'Authorization': `Basic ${auth}`,
  }
};

const websocket = new WebSocket(url, options);
websocket.on('error', console.error);
const xapi = new XAPI(new WSBackend(websocket));

xapi.status.get('Audio Volume')
  .then(console.log)
  .catch(console.error);

xapi.status.on('Audio Volume', console.log);

Output (first prints out the current volume, then prints out the updated status from the codec when the volume changes).

MOHM-M-D2KG:NodeJS mohm$ node ws-jsxapi.js
60
70 { Status: { Audio: { Volume: 70 } }, Id: 0 }
60 { Status: { Audio: { Volume: 60 } }, Id: 0 }
70 { Status: { Audio: { Volume: 70 } }, Id: 0 }
80 { Status: { Audio: { Volume: 80 } }, Id: 0 }
90 { Status: { Audio: { Volume: 90 } }, Id: 0 }

 

NEW: PYXOWS - XoWS Python Library for Python 3.7 =<

Download/Clone here: https://github.com/cisco-ce/pyxows

pyxows is a Python library or XoWS written by the super hero Morten Minde Neergaard and wraps around the XoWS feature to make it easier for you to get started! This library requires Python3.7 and above.

mohm$ git clone https://github.com/cisco-ce/pyxows
Cloning into 'pyxows'...
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (11/11), done.
remote: Total 15 (delta 2), reused 15 (delta 2), pack-reused 0
Unpacking objects: 100% (15/15), done.
pyxows mohm$ python3.7 setup.py install

Done! Now I can use the pyxows library for interacting with codec(s) over XoWS. Let´s get started!

One of the features with pyxows is that it comes with a CLI tool "clixows".

mohm$ clixows
Usage: clixows [OPTIONS] HOST_OR_URL COMMAND [ARGS]...

  First argument is hostname, or url (e.g. ws://example.host/ws)

  Usage examples:

  clixows ws://example.codec/ws get Status SystemUnit Uptime

  clixows example.codec set Configuration Audio Ultrasound MaxVolume 70

  clixows example.codec command Phonebook Search Limit=1 Offset=0

  clixows example.codec feedback -c '**'

Options:
  --version            Show the version and exit.
  -u, --username TEXT  [default: admin]
  -p, --password TEXT  [default: ]
  --help               Show this message and exit.

Commands:
  command   Run a command.
  demo      Runs a quick demo, read source to see...
  feedback  Listen for feedback on a particular query.
  get       Get data from a config/status path.
  query     Query config/status docs.
  set       Set a single configuration.
mohm$ clixows -u admin1 10.10.10.1 feedback -c Status Audio Volume
Subscription Id: 0
{'Status': {'Audio': {'Volume': 40}}}

With clixows you can instantly start a feedback subscription in the terminal or run configurations and commands.

mohm$ clixows -u admin1 10.10.10.1 command Audio Volume Set Level=50
{'status': 'OK'}
mohm$ clixows -u admin1 10.10.10.1 set Configuration Audio DefaultVolume 60
True
mohm$ clixows -u admin1 1010.10.1 query Status '**' DisplayName
{'Status': {'SystemUnit': {'Software': {'DisplayName': 'RoomOS 2019-04-26 '
                                                       '363d7728a2a'}}}}
mohm$ clixows -u admin1 10.10.10.1 query Status '**' Volume
{'Status': {'Audio': {'Output': {'LocalOutput': [{'VolumeControlled': 'On',
                                                  'id': 2},
                                                 {'VolumeControlled': 'Off',
                                                  'id': 8},
                                                 {'VolumeControlled': 'Off',
                                                  'id': 9}]},
                      'Volume': 50,
                      'VolumeMute': 'Off'}}}

To authenticate with a password you add an additional flag -p <password>. This was just a short demo of the CLI function (that uses WebSocket for the connection).

But now over to the python examples to create some practical scenarios, and listen to multiple systems at once:

pyxows-demo.py

import xows
import asyncio

async def start():
async with xows.XoWSClient('10.10.10.1', username='admin1', password='') as client:
def callback(data, id_):
print(f'Feedback (Id {id_}): {data}')

print('Status Query:',
await client.xQuery(['Status', '**', 'DisplayName']))

print('Get:',
await client.xGet(['Status', 'Audio', 'Volume']))

print('Command:',
await client.xCommand(['Audio', 'Volume', 'Set'], Level=60))

print('Configuration:',
await client.xSet(['Configuration', 'Audio', 'DefaultVolume'], 50))

print('Subscription 0:',
await client.subscribe(['Status', 'Audio', 'Volume'], callback, True))

await client.wait_until_closed()

asyncio.run(start())

Callback is the function that is ran every time we receive feedback from the subscription. You can create several subscriptions on one device by adding more susbcriptions. I set True to report the current value of the feedback setting I am listening for.

print('Subscription 1:',
await client.subscribe(['Status', 'Audio', 'Microphones', 'Mute'], callback, True))

When something happens on the feedback we can parse it in the callback function. I have modified the function a bit here, I am checking the first feedback id (id 0) if the audio volume goes above 60, set it back.

async def callback(data, id_):
print(f'Feedback (Id {id_}): {data}')
if id_ == 0:
if data['Status']['Audio']['Volume'] > 60:
await client.xCommand(['Audio', 'Volume', 'Set'], Level=60)

Output currently looks like this when I increase the volume to 65:

Status Query: {'Status': {'SystemUnit': {'Software': {'DisplayName': 'RoomOS 2019-04-26 363d7728a2a'}}}}
Get: 65
Command: {'status': 'OK'}
Configuration: True
Subscription 0: 0
Feedback (Id 0): {'Status': {'Audio': {'Volume': 60}}}
Subscription 1: 1
Feedback (Id 1): {'Status': {'Audio': {'Microphones': {'Mute': 'Off'}}}}
Feedback (Id 0): {'Status': {'Audio': {'Volume': 65}}}
Feedback (Id 0): {'Status': {'Audio': {'Volume': 60}}}
Feedback (Id 0): {'Status': {'Audio': {'Volume': 65}}}
Feedback (Id 0): {'Status': {'Audio': {'Volume': 60}}}
Feedback (Id 0): {'Status': {'Audio': {'Volume': 65}}}
Feedback (Id 0): {'Status': {'Audio': {'Volume': 60}}}

The request/response is close to realtime and very cool to work with, and with the async aspect of this you can create several instances with multiple devices at the same time. Lets do some more modifications to the script so it looks like this:

import xows
import asyncio

async def start(ip, usr, pw, name):
async with xows.XoWSClient(ip, username=usr, password=pw) as client:
async def callback(data, id_):
print(name + f' Feedback (Id {id_}): {data}')
if id_ == 0:
if data['Status']['Audio']['Volume'] > 60:
await client.xCommand(['Audio', 'Volume', 'Set'], Level=60)

print(name + ' Status Query:',
await client.xQuery(['Status', '**', 'DisplayName']))

print(name + ' Get:',
await client.xGet(['Status', 'Audio', 'Volume']))

print(name + ' Command:',
await client.xCommand(['Audio', 'Volume', 'Set'], Level=60))

print(name + ' Configuration:',
await client.xSet(['Configuration', 'Audio', 'DefaultVolume'], 50))

print(name + ' Subscription 0:',
await client.subscribe(['Status', 'Audio', 'Volume'], callback, True))

print(name + ' Subscription 1:',
await client.subscribe(['Status', 'Audio', 'Microphones', 'Mute'], callback, True))

await client.wait_until_closed()

async def task():
codecs = [('10.10.10.1', 'admin1', '', 'KitPro'),
('10.10.10.2', 'admin', '', 'MX300 G2')]
connections = [start(*codec) for codec in codecs]

await asyncio.wait(connections)

asyncio.run(task())

Now we setup a connection running the same task on both devices, all I need to do is to add mode devices to the "codecs" object. See below for the output (nice and async :) ):

MX300 G2 Status Query: {'Status': {'Call': [{'id': 7, 'DisplayName': ''}], 'SIP': {'CallForward': {'DisplayName': ''}}, 'SystemUnit': {'Software': {'DisplayName': 'ce 9.8.0 ece17b2774f 2019-04-02'}}}}
KitPro Status Query: {'Status': {'SystemUnit': {'Software': {'DisplayName': 'RoomOS 2019-04-26 363d7728a2a'}}}}
MX300 G2 Get: 60
KitPro Get: 60
MX300 G2 Command: {'status': 'OK'}
KitPro Command: {'status': 'OK'}
MX300 G2 Configuration: True
KitPro Configuration: True
MX300 G2 Subscription 0: 0
MX300 G2 Feedback (Id 0): {'Status': {'Audio': {'Volume': 60}}}
MX300 G2 Subscription 1: 1
MX300 G2 Feedback (Id 1): {'Status': {'Audio': {'Microphones': {'Mute': 'Off'}}}}
KitPro Subscription 0: 0
KitPro Feedback (Id 0): {'Status': {'Audio': {'Volume': 60}}}
KitPro Subscription 1: 1
KitPro Feedback (Id 1): {'Status': {'Audio': {'Microphones': {'Mute': 'Off'}}}}

With these examples in place I hope you are able to take advantage of the pyxows library and create something awesome. The above works like a charm for test sequences, provisioning of multiple commands / configs and status checks etc. Use with Macros and In-Room Control? Go ahead!

Have fun, and please ask if you have questions.

Implementation


This is an integration feature like the XMLAPI its a different way of communicating with the xAPI but with the speedy benefits of WebSockets and pure JSON communication. Feel free to build on the examples in this blog post and create something awesome!

Known Issues & Limitations


Degraded error handling for WebSocket - improvment request for this is filed. Application errors will also be better documented in the future for those that wants to build their own WebSocket implementations.

Troubleshooting, diagnostics, limitations and advisories


When a new configuration or command is issues via WebSocket you will see this in the endpoint logs with the user that issued the command.

2019-02-01T14:52:10.073+01:00 appl[2177]: CuilApp[1]: User remotesupport/websocket about to execute command '/UserInterface/OSD/Key/Click Key: Left' from 10.10.10.5.
2019-02-01T14:52:12.078+01:00 appl[2177]: CuilApp[1]: User remotesupport/websocket about to execute command '/UserInterface/OSD/Key/Click Key: Right' from 10.10.10.5.
2019-02-01T14:52:14.082+01:00 appl[2177]: CuilApp[1]: User remotesupport/websocket about to execute command '/UserInterface/OSD/Key/Click Key: Up' from 10.10.10.5.
2019-02-01T14:52:16.087+01:00 appl[2177]: CuilApp[1]: User remotesupport/websocket about to execute command '/UserInterface/OSD/Key/Click Key: Down' from 10.10.10.5.

 

 

2 Comments
Beginner

Magnus,

As usual, you created an excellent document!  Thank you for putting these mini-guides/tutorials for us.

Beginner

Websockets are a great idea if they’re implemented properly. Do you know if they are strictly following RFC-6455 and are they going to force TLS or can we test insecure and then add TLS before wide scale deployment?

CreatePlease to create content
Content for Community-Ad
August's Community Spotlight Awards
This widget could not be displayed.