Skip to content

Latest commit

 

History

History
426 lines (300 loc) · 15.7 KB

README.md

File metadata and controls

426 lines (300 loc) · 15.7 KB

Workspace Integrations

Node.js SDK for creating Webex Workspace Integrations.

Poster

The workspace integrations is a cloud based framework that lets you connect to the Cisco devices, receive sensor data, invoke commands, read status values and update configs in a secure and controlled manner, without installing anything on the devices.

Warning This SDK is in progress and not yet at version 1.0. Changes in the API must be expected. Also, there may be errors and typos in examples and documentation. Be sure to use the Github Issues to give feedback.

The SDK is designed to be similar to the macros and JSXAPI syntax, so developers that are experienced with that can easily start using Workspace Integrations too.

Features

  • Quick and easy to set up, just plug in the OAuth details you get from Control Hub
  • Automatically handles access tokens for you, including refreshing it on time
  • Object-oriented API, no need to deal with HTTP calls
  • Access device data:
    • Querying for status
    • Getting and setting device configurations
    • Invoking commands
    • Subscribing to events and status changes
  • Support for both long polling and web hook for events and status updates

See full API reference here.

Installation

Install as any npm package:

npm install --save workspace-integrations

Prelude: Adding and activating an integration

Before you are able to run an integration towards Webex, you will need to add and activate it in Control Hub. The basic steps of this is:

  • Add integration
  • Copy and save the app id and app secret
  • Activate the integration
  • Copy and save the activation code (JSON Web Token)

Once you have the activation code, you can use this SDK to decode and verify it. Decoding is necessary to get the last configs (refresh tokens, app urls etc), and verifying is recommended for security reasons.

jwt-decode is just a command-line tool that the SDK provides for your convenience. IF you want, you can decode and validate the JWT yourself, eg on https://jwt.io.

cd to the folder where you installed the workspace-integrations sdk, and do:

npx jwt-decode <activation-code>

You should now get back something like:

🎉 JWT Successfully verified. Copy and paste the data below for connecting your integration:

{
  "oauthUrl": "https://webexapis.com/v1/access_token",
  "appUrl": "https://xapi-a.wbx2.com/xapi/api/organizations/4590eb6a...",
  "webexapisBaseUrl": "https://webexapis.com/v1",
  "refreshToken": "NmRlOGIw..."
}

You can just type jwt-decode to get help and more output options.

Now you need to combine the data you've copied to a config that looks like this, for connecting the integration:

const config = {
  clientId: "C12ba...",
  clientSecret: "fdbcd00...",
  activationCode: {
    oauthUrl: '...',
    refreshToken: '...',
    webexapisBaseUrl: '...',
    appUrl: '...',
  },
  notifications: 'longpolling',
};

Getting started

Show a message on the device screen each time the people count changes:

const { connect } = require('workspace-integrations');

// See above on how to find these values:
const config = {
  clientId: "C12ba...",
};

async function init() {
  try {
    const integration = await connect(config);
    integration.xapi.status.on('RoomAnalytics PeopleCount Current', (deviceId, path, value) => {
      const msg = `Number of people in the room: ${value}`;
      showMessageOnScreen(integration.xapi, deviceId, msg);
    });
  }
  catch(e) {
    console.log('Something went wrong', e);
  }
}

function showMessageOnScreen(xapi, deviceId, text) {
   const args = {
     Text: text,
     Duration: 5,
   };
   xapi.command(deviceId, 'UserInterface Message Alert Display', args);
}

init();

Be aware that any status, event or command used in a workspace integration also needs to be specified in the manifest. Specifying it in code is not enough be itself, and the SDK will not throw any errors if you for example subscribe to a status change that is not listed in the manifest.

You can find a graphical editor for the manifest that makes this easier on https://cisco-ce.github.io/workspace-integrations-editor/.

A couple of more syntax examples (the xapi and deviceId is found similar to the above example):

// Get current volume:
const value = await integration.xapi.status.get(deviceId, 'Audio Volume');
console.log('Volume:', volume);


// Subscribe to analytics data
// Note: Don't use star as wildcard, it supports partial match similar to JSXAPI
integration.xapi.status.on('RoomAnalytics', (deviceId, name, value) => {
  console.log('Room Analytics updated', name, value);
});

Command details

Sometimes you may need the result of commands. This is returned as a normal async answer.

// Search the phone book:
const res = await integration.xapi.command(device, 'Phonebook Search', { PhonebookType: 'Local', Limit: 10 });
console.log('phonebook', res.Contact);

Commands with multi-line content (such as images, xml or other data blobs) can be set using the fourth parameter:

const data = 'const data = 1; \n const moreData = 2;';
try {
  await integration.xapi.command(deviceId, 'Macros Macro Save', { Name: 'mymacro' }, data);
}
catch(e) {
  console.log('Not able to write macro', e);
}

Configurations

Note: setting a configuration requires the spark-admin:devices_write scope to be set in the manifest.

// Read a config:
const mode = await integration.xapi.config.get(deviceId, 'RoomAnalytics.PeoplePresenceDetector')
console.log('Detector:', mode);

// Set a config
// NOTE:
try {
  await integration.xapi.config.set(deviceId, 'RoomAnalytics.PeoplePresenceDetector', 'On');
}
catch(e) {
  console.log('Not able to set config', e);
}

You can also set multiple configs in one go with the setMany function:

const configs = {
  'Audio.Ultrasound.MaxVolume': 0,
  'Audio.DefaultVolume': 33,
  'Audio.SoundsAndAlerts.RingVolume': 66,
};
await integration.xapi.config.setMany(device, configs);

Note that the configuration apis do not actually need to be specified in the manifest. Unlike status, commands and statuses there is no granular control.

Discovering devices

The SDK also allow you to find devices in your organization.

// Show all devices in your org:
const devices = await integration.devices.getDevices();
const names = devices.map(d => `${d.displayName} (${d.product} ${d.type})`);
console.log(names);

// TODO
// Get the devices in the location that your integration has been enabled for
// xapi.getLocations().forEach(async (locationId) => {
//   const devices = await xapi.getDevices(locationId, { type: 'roomdesk' });
//   console.log(devices);
// });

// the filterting options (like 'roomdesk') are the same as described on
// https://developer.webex.com/docs/api/v1/devices/list-devices

Necessary API scopes

For the SDK to work, you typically need to add the following API scopes to your manifest:

  • spark-admin:workspaces_read - Get list of workspaces, and workspace details
  • spark-admin:devices_read - Get list of devices, and device details
  • spark:xapi_statuses - Query and subscribe to xAPI status
  • spark:xapi_commands - Invoke xAPI commands

You can also read and change device configurations if you use spark-admin:devices_read and spark-admin:devices_write.

Long polling and web hooks

Workspace integrations support two different method of receiving data from the devices / Webex:

  1. Long polling

The integration is itself responsible for continuously asking Webex for updates. This integration does not require a public web server, and can therefore be run inside a protected intranet. It's a simple mechanism that is handled entirely by the SDK itself.

  1. Web hooks

The integration is hosted on a public site (must be https), and receives the device data as web hooks from Webex. Typically needed if you want to provide a public integration that customers can pay for and use, without hosting anything themselves.

Log level

You can choose which level the SDK reports logs at. The default level is error, so it's only errors that are shown in the console, but if you want to see more of what is happening, you can change it in your config:

const config = {
  appId: '...',
  logLevel: 'info', //
}

connect(config);

Web hooks

It is also possible to use the web hook deployment model with the SDK. In this case, you need to provide the web server yourself, then feed the incoming web hook data from Webex to the SDK using integration.processNotifications(). The SDK will then deliver the events and status update to your listeners, exactly in the same way as with long polling.

The following example shows how to do this with a simple Express web server, but you can of course use any web server you prefer.

const express = require('express');
const bodyParser = require('body-parser');
const { connect } = require('workspace-integrations');

const app = express();

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

const port = 80;

// This is the public URL you manage.
const url = 'https://acme.com';

const config = {
  clientId: 'xxx',
  clientSecret: 'yyy',
  activationCode: 'zzz',
  notifications: 'webhook',
  webhook: {
    targetUrl: `${url}/api/webhooks`, // you can choose the route yourself
    type: 'hmac_signature',
    secret: 'somethingmorethan20chars'
  },
  actionsUrl: `${url}/api/webexnotify`, // you can choose the route yourself
};

let integration;

// the route here must be the same as you specify in webhook above
app.all('/api/webhooks', (req, res) => {
  const { method, body, headers } = req;
  if (integration) {
    integration.processNotifications(body);
  }

  res.send('ok');
});

app.listen(port, () => console.log(`http server on port ${port}`));

function onConnect(_integration) {
  console.log('connected, xapi ready');
  integration = _integration;

  // this shows that subscriptions are working, just like for long polling:
  integration.xapi.event.on('', (device, path, data) => console.log('SDK event:', path, data, device));
  integration.xapi.status.on('', (device, path, data) => console.log('SDK status:', path, data, device));
}

connect(config)
  .then(onConnect)
  .catch(e => console.log('Error!', e))

Tip: For testing web hooks during development, you can use https://ngrok.com/.

Type script support

The SDK has TypeScript definitions and should work out of the box. If you are using vanilla JS and not TypeScript, you should still be able to get auto completions in Visual Studio Code or any other editors that support the .d.ts type definitions.

Custom Webex API calls

This SDK contains Webex API wrappers mostly for dealing with devices (xAPI). However, there are a ton of other Webex APIs you may wish to use in your application. For this you need the base URL and the token, and the SDK can help you with this. You need to remember to add the relevant scopes to your manifest too.

The SDK uses node-fetch for the actual calls. In addition, it throws an error if the result of the HTTP query is not ok (HTTP 2xx status code).

Here's an example of how to use the People API to lookup people in your organisation. This also requires the spark-admin:people_read scope.

async function findPerson(integration, name) {
  const url = 'people/?displayName=' + name;
  const data = await integration.webexApi(url);
  console.log('Found:', data.items);
}

Behind the scenes, the SDK automatically:

  • Adds the Webex api base url (may change if the integration is FedRAMP)
  • Adds the access token to the header, and sets content type to JSON
  • Converts the result from JSON string to JavaScript object
  • Throws an exception if the call is unsuccessful

Limitations

Please be aware of the following limitations:

  • There's a limited set of statuses and events you can subscribe to, such as room analytics and user interface extensions events (you can see the up-to-date list on Control Hub or in the Manifest editor). For the statuses, you can still query all of them.

  • On personal devices, you cannot use APIs that are listed as privacy impacting (see roomos.cisco.com/xapi to verify the APIs).

  • If your integration has been allowed for only certain locations, you will still be able to list all the devices in the org and manipulate configs, but only invoke commands and statuses for the device in the allowed locations. Other devices will return 403 Forbidden.

  • Personal devices will not send any events at all.

Clustered integrations

For distributed use cases, the SDK supports a cluster-based architecture by allowing the integration to be serialized and deserialized. This means that you need to save the serialized state (which contains access tokens, refresh tokens etc) and provide this to your server node when activating them.

Note that the application itself is now responsible for refreshing a token if it expires while no nodes are active. The serialized object contains a tokenExpiryTime string (ISO date) indicating when the access token expires.

Here's an example of the functions you need to use:

const { connect, deserialize, createAccessToken } = require('workspace-integrations');

// normal connect call when initialising an org/app 
const integration = await connect(yourActivationConfig);

// serialize your org/app instance (JSON object that can be persisted):
const savedIntegration = integration.serialize();

// instantiate an integration object, it is now ready to use xapi etc immediately
const integration = deserialize(savedIntegration)

// If you need to refresh token yourself while no nodes are active
// The clientId etc are found in the serialized state bundle: 
const { access_token } = await createAccessToken(clientId, clientSecret, oauthUrl, refreshToken)

The clustered integration only makes sense when your application is using the web hook strategy.

Contributing to the SDK

This SDK is an open source project and contributions are of course welcome! See the contributor article for more info.

Useful resources