├── .gitignore ├── LICENSE ├── README.md ├── controls ├── README.md ├── agenda │ ├── Agenda-CLEUR-About.png │ ├── Agenda-CLEUR-Thursday.png │ ├── Agenda-CLEUR-Tuesday.png │ ├── Agenda-CLEUR-Wednesday.png │ ├── README.md │ ├── Touch10-Home.png │ ├── agenda-CLANZ.xml │ └── agenda-CLEUR.xml ├── agenda_notif │ ├── README.md │ ├── agenda.js │ ├── agenda.xml │ ├── img │ │ ├── incoming_webhook.png │ │ ├── push_to_teams.png │ │ └── touch10_snapshot.png │ ├── package.json │ └── util │ │ └── sessions.js ├── agenda_post │ ├── README.md │ ├── agenda.js │ ├── agenda.xml │ ├── img │ │ ├── push_to_teams.png │ │ └── touch10_snapshot.png │ └── package.json ├── family_link │ ├── README.md │ ├── WebexQuickDial.js │ ├── WebexQuickDial.xml │ └── docs │ │ └── ICU-DX.png ├── hue │ ├── README.md │ ├── control.xml │ ├── docs │ │ ├── control.png │ │ └── light.png │ ├── macro.js │ └── updateLight.js ├── hue_disco │ ├── README.md │ ├── control.xml │ └── macro.js ├── maze │ ├── README.md │ ├── map.png │ ├── maze.js │ ├── maze.png │ └── maze.xml ├── maze_french │ ├── README.md │ ├── game.png │ ├── levels-french.js │ ├── levels-french.xml │ ├── options.png │ └── rules.png ├── maze_levels │ ├── README.md │ ├── game.png │ ├── levels.js │ ├── levels.xml │ ├── map.png │ ├── options.png │ └── rules.png ├── maze_scores │ ├── README.md │ ├── game.png │ ├── package.json │ ├── rules.png │ ├── score-jsxapi.js │ ├── score.js │ └── score.xml ├── notifier │ ├── README.md │ ├── img │ │ ├── panel_notify.png │ │ ├── panel_recipient.png │ │ ├── push_to_teams.png │ │ └── touch10_home.png │ ├── notifier.js │ ├── notifier.xml │ ├── package.json │ └── to_webex_teams.js ├── onair │ ├── README.md │ ├── control.xml │ ├── macro.js │ ├── multi.js │ ├── package.json │ └── script.js ├── proximity │ ├── README.md │ ├── proximity.js │ ├── proximity.png │ └── proximity.xml └── ultrasound │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── ultrasound-macro.js │ ├── ultrasound.js │ ├── ultrasound.png │ └── ultrasound.xml ├── docs ├── QuickStart.md └── img │ ├── controls-import-from-file.png │ ├── controls-push-to-device.png │ ├── macro-activate.png │ ├── macro-import-from-file.png │ └── playground │ ├── 1-playground-first-launch.png │ ├── 2-playground-import-file.png │ ├── 3-playground-after-import.png │ └── 4-playground-preview.png ├── jsxapi ├── .gitignore ├── 0-quickstart.js ├── 1-connect.js ├── 10-autoanswer.js ├── 11-update-wallpaper.js ├── 12-encrypt-nokeygen.js ├── 12-encrypt.js ├── 2-environment.js ├── 3-dial.js ├── 4-custom-messages.js ├── 5-rolling-messages.js ├── 6-branding-logo.js ├── 7-feedback-peoplecount.js ├── 8-listen-audio-volume.js ├── 9-listen-all-status.js ├── README.md ├── docs │ ├── 4.0.0 │ │ ├── ast │ │ │ └── source │ │ │ │ ├── backend │ │ │ │ ├── index.js.json │ │ │ │ ├── tsh.js.json │ │ │ │ └── ws.js.json │ │ │ │ ├── cli.js.json │ │ │ │ ├── index.js.json │ │ │ │ ├── json-parser.js.json │ │ │ │ ├── log.js.json │ │ │ │ ├── transport │ │ │ │ ├── ssh.js.json │ │ │ │ ├── stream.js.json │ │ │ │ └── tsh.js.json │ │ │ │ └── xapi │ │ │ │ ├── components.js.json │ │ │ │ ├── exc.js.json │ │ │ │ ├── feedback.js.json │ │ │ │ ├── index.js.json │ │ │ │ ├── mixins.js.json │ │ │ │ └── rpc.js.json │ │ ├── badge.svg │ │ ├── class │ │ │ └── src │ │ │ │ ├── backend │ │ │ │ ├── index.js~Backend.html │ │ │ │ ├── tsh.js~TSHBackend.html │ │ │ │ └── ws.js~WSBackend.html │ │ │ │ ├── json-parser.js~JSONParser.html │ │ │ │ ├── transport │ │ │ │ └── stream.js~StreamTransport.html │ │ │ │ └── xapi │ │ │ │ ├── components.js~Component.html │ │ │ │ ├── components.js~Config.html │ │ │ │ ├── components.js~Event.html │ │ │ │ ├── components.js~Status.html │ │ │ │ ├── exc.js~IllegalValueError.html │ │ │ │ ├── exc.js~InvalidPathError.html │ │ │ │ ├── exc.js~ParameterError.html │ │ │ │ ├── exc.js~XAPIError.html │ │ │ │ ├── feedback.js~Feedback.html │ │ │ │ ├── feedback.js~FeedbackGroup.html │ │ │ │ ├── index.js~XAPI.html │ │ │ │ ├── mixins.js~Gettable.html │ │ │ │ ├── mixins.js~Listenable.html │ │ │ │ └── mixins.js~Settable.html │ │ ├── coverage.json │ │ ├── css │ │ │ ├── prettify-tomorrow.css │ │ │ └── style.css │ │ ├── dump.json │ │ ├── file │ │ │ └── src │ │ │ │ ├── backend │ │ │ │ ├── index.js.html │ │ │ │ ├── tsh.js.html │ │ │ │ └── ws.js.html │ │ │ │ ├── cli.js.html │ │ │ │ ├── index.js.html │ │ │ │ ├── json-parser.js.html │ │ │ │ ├── log.js.html │ │ │ │ ├── transport │ │ │ │ ├── ssh.js.html │ │ │ │ ├── stream.js.html │ │ │ │ └── tsh.js.html │ │ │ │ └── xapi │ │ │ │ ├── components.js.html │ │ │ │ ├── exc.js.html │ │ │ │ ├── feedback.js.html │ │ │ │ ├── index.js.html │ │ │ │ ├── mixins.js.html │ │ │ │ └── rpc.js.html │ │ ├── function │ │ │ └── index.html │ │ ├── identifiers.html │ │ ├── image │ │ │ ├── badge.svg │ │ │ ├── github.png │ │ │ └── search.png │ │ ├── index.html │ │ ├── package.json │ │ ├── script │ │ │ ├── inherited-summary.js │ │ │ ├── inner-link.js │ │ │ ├── manual.js │ │ │ ├── patch-for-local.js │ │ │ ├── prettify │ │ │ │ ├── Apache-License-2.0.txt │ │ │ │ └── prettify.js │ │ │ ├── pretty-print.js │ │ │ ├── search.js │ │ │ ├── search_index.js │ │ │ └── test-summary.js │ │ ├── source.html │ │ └── variable │ │ │ └── index.html │ └── 4.2.0 │ │ ├── ast │ │ └── source │ │ │ ├── .external-ecmascript.js.json │ │ │ ├── backend │ │ │ ├── index.js.json │ │ │ ├── tsh.js.json │ │ │ └── ws.js.json │ │ │ ├── cli.js.json │ │ │ ├── index.js.json │ │ │ ├── json-parser.js.json │ │ │ ├── log.js.json │ │ │ ├── transport │ │ │ ├── ssh.js.json │ │ │ ├── stream.js.json │ │ │ └── tsh.js.json │ │ │ └── xapi │ │ │ ├── components.js.json │ │ │ ├── exc.js.json │ │ │ ├── feedback.js.json │ │ │ ├── index.js.json │ │ │ ├── mixins.js.json │ │ │ ├── normalizePath.js.json │ │ │ └── rpc.js.json │ │ ├── badge.svg │ │ ├── class │ │ └── src │ │ │ ├── backend │ │ │ ├── index.js~Backend.html │ │ │ ├── tsh.js~TSHBackend.html │ │ │ └── ws.js~WSBackend.html │ │ │ ├── json-parser.js~JSONParser.html │ │ │ ├── transport │ │ │ └── stream.js~StreamTransport.html │ │ │ └── xapi │ │ │ ├── components.js~Component.html │ │ │ ├── components.js~Config.html │ │ │ ├── components.js~Event.html │ │ │ ├── components.js~Status.html │ │ │ ├── exc.js~IllegalValueError.html │ │ │ ├── exc.js~InvalidPathError.html │ │ │ ├── exc.js~ParameterError.html │ │ │ ├── exc.js~XAPIError.html │ │ │ ├── feedback.js~Feedback.html │ │ │ ├── feedback.js~FeedbackGroup.html │ │ │ ├── index.js~XAPI.html │ │ │ ├── mixins.js~Gettable.html │ │ │ ├── mixins.js~Listenable.html │ │ │ └── mixins.js~Settable.html │ │ ├── coverage.json │ │ ├── css │ │ ├── github.css │ │ ├── identifiers.css │ │ ├── manual.css │ │ ├── prettify-tomorrow.css │ │ ├── search.css │ │ ├── source.css │ │ ├── style.css │ │ └── test.css │ │ ├── file │ │ └── src │ │ │ ├── backend │ │ │ ├── index.js.html │ │ │ ├── tsh.js.html │ │ │ └── ws.js.html │ │ │ ├── cli.js.html │ │ │ ├── index.js.html │ │ │ ├── json-parser.js.html │ │ │ ├── log.js.html │ │ │ ├── transport │ │ │ ├── ssh.js.html │ │ │ ├── stream.js.html │ │ │ └── tsh.js.html │ │ │ └── xapi │ │ │ ├── components.js.html │ │ │ ├── exc.js.html │ │ │ ├── feedback.js.html │ │ │ ├── index.js.html │ │ │ ├── mixins.js.html │ │ │ ├── normalizePath.js.html │ │ │ └── rpc.js.html │ │ ├── function │ │ └── index.html │ │ ├── identifiers.html │ │ ├── image │ │ ├── badge.svg │ │ ├── esdoc-logo-mini-black.png │ │ ├── esdoc-logo-mini.png │ │ ├── manual-badge.svg │ │ └── search.png │ │ ├── index.html │ │ ├── index.json │ │ ├── lint.json │ │ ├── script │ │ ├── inherited-summary.js │ │ ├── inner-link.js │ │ ├── manual.js │ │ ├── patch-for-local.js │ │ ├── prettify │ │ │ ├── Apache-License-2.0.txt │ │ │ └── prettify.js │ │ ├── pretty-print.js │ │ ├── search.js │ │ ├── search_index.js │ │ └── test-summary.js │ │ ├── source.html │ │ └── variable │ │ └── index.html ├── httpfeedback │ ├── README.md │ ├── package.json │ └── server.js ├── img │ ├── cisco-logo-white.png │ └── create-logo-transparent.png └── package.json ├── macros ├── 1-helloworld.js ├── 10-httpclient.js ├── 11-philips-hue.js ├── 12a-primary.js ├── 12b-secondary.js ├── 13a-environment.js ├── 13b-getenv.js ├── 14-persist.js ├── 15-cipher.js ├── 16-encrypt-rsa.js ├── 17-persistenv.js ├── 18-hue_disco.js ├── 19-hue_onair.js ├── 2-showVolume.js ├── 20-import_main.js ├── 20-import_util.js ├── 3-changeLanguage.js ├── 4-webhook.js ├── 4b-webhook-controls.js ├── 5-checkEmail.js ├── 6-maze.js ├── 7-showProgress.js ├── 8-message.js ├── 9-timer.js ├── README.md └── pdf │ └── macro-tutorial.pdf ├── python ├── .gitignore ├── advanced.py ├── requirements.txt └── websocket.py ├── pyxows ├── README.md ├── connection_timeout.py ├── control_volume.py └── usage.py └── web ├── README.md ├── example.html └── jsxapi-501.min.js /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | node_modules/ 3 | work/ 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Cisco Systems 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Handy samples for xAPI UI Extensions and Macros [![published](https://static.production.devnetcloud.com/codeexchange/assets/images/devnet-published.svg)](https://developer.cisco.com/codeexchange/github/repo/CiscoDevNet/xapi-samples) 2 | 3 | This repo can get you quickly ramp up with CE programmability and xAPI, with examples for: 4 | - [UI Extensions](./controls), 5 | - [JavaScript Macros](./macros), 6 | - [Node.js applications](./jsxapi), 7 | - [Python applications](./pyxows) 8 | 9 | **New to xAPI UI Extensions and Macros?** 10 | - check the [QuickStart Guide](./docs/QuickStart.md) to learn to load Controls and Macros to your device, 11 | - take a DevNet Tutorial from the xAPI track (intro to xAPI and creating custom UI Extensions) 12 | 13 | **Don't have a CE device at hand to mess up with?** 14 | - reserve a [DevNet sandbox](https://github.com/CiscoDevNet/awesome-xapi/#sandboxes) equiped with CE latest 15 | 16 | **Going Futher** 17 | Once you're done browsing the examples in this repo, here are a few suggestions 18 | - check the [official Macro Samples repository](https://github.com/CiscoDevNet/roomdevices-macros-samples) 19 | - load the [Postman collection for xAPI](https://github.com/CiscoDevNet/postman-xapi) to invoke the xAPI from code external to the Room Device 20 | - read through the full [CE Customization PDF Guide](https://www.cisco.com/c/dam/en/us/td/docs/telepresence/endpoint/ce99/collaboration-endpoint-software-api-reference-guide-ce99.pdf) 21 | - check for the curated list of resources at [awesome-xapi](https://github.com/CiscoDevNet/awesome-xapi) 22 | 23 | 24 | ## T-Shell Tips for developers 25 | 26 | Simply SSH to your Collaboration Device and run the commands below: 27 | 28 | ### Listen to events 29 | 30 | The 'xfeedback' commands let you see all events fired on your device. 31 | This is very useful to investigate possibilities, and take shortcut without going through the whole documentation at times. 32 | 33 | ```shell 34 | # Listen to all notifications (events, status, commands) 35 | xfeedback register / 36 | ``` 37 | 38 | ```shell 39 | # Listen to UI Extensions events 40 | xfeedback register /Event/UserInterface/Extensions 41 | ``` 42 | 43 | ```shell 44 | # Stop listening 45 | xfeedback deregisterall 46 | ``` 47 | 48 | ### Send messages 49 | 50 | Sending messages lets you craft custom APIs, by coming up with your own protocols, aka, Event Driven Architectures. 51 | One code will send a serialized message, the other code will capture the message and decode it. 52 | 53 | ```shell 54 | # Listen to messages 55 | xfeedback register /Event/Message/Send 56 | ``` 57 | 58 | ```shell 59 | # Send message 60 | xCommand Message Send Text: "This is random text" 61 | ``` 62 | 63 | Check the [message Macro](macros/8-message.js) for an example in JavaScript. -------------------------------------------------------------------------------- /controls/README.md: -------------------------------------------------------------------------------- 1 | # Examples of In-Room Controls 2 | 3 | Run these samples to explore a few of the possibilities of your Room Device programmability: 4 | 5 | **New to Controls & Macros? check the [QuickStart Guide](../docs/QuickStart.md) to learn to load Controls and Macros to your device** 6 | 7 | - [Agenda](./agenda): example of a control showing static information 8 | - [Ultrasound](./ultrasound): helper panel to update the Ultrasound volume 9 | - [Proximity](./proximity): helper panel to toggle Proximity Mode on/off 10 | - [Maze](./maze): Play an interactive game where you navigate blind in a maze, looking for a treasure. Check the more advanced versions with [a scoring system](./maze_scores) and [difficulty levels](./maze_levels) 11 | 12 | -------------------------------------------------------------------------------- /controls/agenda/Agenda-CLEUR-About.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/agenda/Agenda-CLEUR-About.png -------------------------------------------------------------------------------- /controls/agenda/Agenda-CLEUR-Thursday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/agenda/Agenda-CLEUR-Thursday.png -------------------------------------------------------------------------------- /controls/agenda/Agenda-CLEUR-Tuesday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/agenda/Agenda-CLEUR-Tuesday.png -------------------------------------------------------------------------------- /controls/agenda/Agenda-CLEUR-Wednesday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/agenda/Agenda-CLEUR-Wednesday.png -------------------------------------------------------------------------------- /controls/agenda/README.md: -------------------------------------------------------------------------------- 1 | # Example of a static Agenda 2 | 3 | **New to Controls & Macros? check the [QuickStart Guide](../../docs/QuickStart.md) to learn to load Controls and Macros to your device** 4 | 5 | 6 | This control does not invoke any macro, simply displays a static agenda. 7 | 8 | ![](./Touch10-Home.png) 9 | 10 | 11 | With the example of my schedule at Cisco Live Europe 2018 - Barcelona 12 | 13 | ![](./Agenda-CLEUR-Tuesday.png) 14 | 15 | ![](./Agenda-CLEUR-Wednesday.png) 16 | 17 | ![](./Agenda-CLEUR-Thursday.png) 18 | 19 | ![](./Agenda-CLEUR-About.png) 20 | -------------------------------------------------------------------------------- /controls/agenda/Touch10-Home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/agenda/Touch10-Home.png -------------------------------------------------------------------------------- /controls/agenda_notif/README.md: -------------------------------------------------------------------------------- 1 | # Agenda with notifications 2 | 3 | This agenda pushes session details to a Webex Teams space (via an incoming Webhook) 4 | 5 | ![](img/touch10_snapshot.png) 6 | 7 | 8 | ## Quickstart 9 | 10 | Deploy the [control](./agenda.xml) to your device 11 | 12 | Create an [incoming webhook](https://apphub.webex.com/categories/other/integrations/incoming-webhooks-cisco-systems) from Webex App Hub Integrations 13 | 14 | Run the JS script. 15 | _Make sure to add an extra `INCOMING_WEBHOOK_ID` env variable filled with the identifier created earlier_ 16 | 17 | ```shell 18 | git clone https://github.com/ObjectIsAdvantag/xapi-samples 19 | cd controls 20 | cd agenda_notif 21 | npm install 22 | INCOMING_WEBHOOK_ID="XXXXX" JSXAPI_DEVICE_URL='ssh://192.168.1.34' JSXAPI_USERNAME='integrator' JSXAPI_PASSWORD='integrator' node agenda.js 23 | ``` 24 | 25 | Press 'push' and see messages poping up in Webex Teams. 26 | 27 | ![](img/push_to_teams.png) -------------------------------------------------------------------------------- /controls/agenda_notif/agenda.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * Listen to UserInterfaace realtime events via xAPI's feedback function 8 | * Pushes events to a Webex Teams space via an incoming Webhook 9 | * Use the INCOMING_WEBHOOK_ID env variable to override default space 10 | */ 11 | 12 | // Incoming webhook id 13 | var webhook_id = process.env.INCOMING_WEBHOOK_ID; 14 | if (!webhook_id) { 15 | console.log("WARNING: no incoming webhook id specified on the command line, using defaults."); 16 | console.log("to join the default Webex Teams space, open https://eurl.io/#H1Cb0XKgQ") 17 | webhook_id = "Y2lzY29zcGFyazovL3VzL1dFQkhPT0svMTI5YzA4ZGQtODJlYy00ZTg2LThkODAtZWFiMjNjYWVmOTRm"; 18 | } 19 | 20 | // 21 | // Connect to the device 22 | // 23 | 24 | const jsxapi = require('jsxapi'); 25 | 26 | // Check args 27 | if (!process.env.JSXAPI_DEVICE_URL || !process.env.JSXAPI_USERNAME) { 28 | console.error("Please specify info to connect to your device as JSXAPI_DEVICE_URL, JSXAPI_USERNAME, JSXAPI_PASSWORD env variables"); 29 | console.error("Bash example: JSXAPI_DEVICE_URL='ssh://192.168.1.34' JSXAPI_USERNAME='integrator' JSXAPI_PASSWORD='integrator' node agenda.js"); 30 | process.exit(1); 31 | } 32 | 33 | // Empty passwords are supported 34 | const password = process.env.JSXAPI_PASSWORD ? process.env.JSXAPI_PASSWORD : ""; 35 | 36 | // Connect to the device 37 | console.log("connecting to your device..."); 38 | const xapi = jsxapi.connect(process.env.JSXAPI_DEVICE_URL, { 39 | username: process.env.JSXAPI_USERNAME, 40 | password: password 41 | }); 42 | xapi.on('error', (err) => { 43 | switch (err) { 44 | case "client-socket": 45 | console.error("could not connect: invalid URL."); 46 | break; 47 | 48 | case "client-authentication": 49 | console.error("could not connect: invalid credentials."); 50 | break; 51 | 52 | case "client-timeout": 53 | console.error("could not connect: timeout."); 54 | break; 55 | 56 | default: 57 | console.error(`encountered error: ${err}.`); 58 | break; 59 | } 60 | 61 | console.log("exiting..."); 62 | process.exit(1); 63 | }); 64 | 65 | 66 | // 67 | // Code logic 68 | // 69 | 70 | xapi.on('ready', () => { 71 | console.log("connexion successful"); 72 | 73 | // Listen to custom In-Room Controls events 74 | console.log("added feedback event listener: UserInterface Extensions Event Clicked"); 75 | xapi.event.on('UserInterface Extensions Event Clicked', (event) => { 76 | console.log(`new event from: ${event.Signal}`); 77 | 78 | // Identify session associated to button 79 | let sessionId = extractSession(event.Signal); 80 | if (!sessionId) { 81 | console.log("bad format, ignoring..."); 82 | return; 83 | } 84 | 85 | // Fetch session details 86 | const getSessionDetails = require('./util/sessions.js') 87 | let session = getSessionDetails(sessionId); 88 | if (!session) { 89 | console.log("could not find details for session, ignoring..."); 90 | return; 91 | } 92 | 93 | // Push info about the session 94 | let href = "https://www.ciscolive.com/us/learn/sessions/session-catalog/?search=" + sessionId; 95 | push(`${session.day}, ${session.time}: **${session.title}** in ${session.location}
_${session.description}_`); 96 | }); 97 | }); 98 | 99 | function extractSession(component) { 100 | //let parsed = component.match(/^push_(DEVNET-\d{4}[A:B]?)$/); 101 | let parsed = component.match(/^push_(.*)$/); 102 | 103 | if (!parsed) { 104 | console.log("format error, please comply with 'push_DEVNET-DDDD'"); 105 | return undefined; 106 | } 107 | 108 | return parsed[1]; 109 | } 110 | 111 | // Post message to a Webex Teams space via an Incoming Webhook 112 | function push(msg, cb) { 113 | var request = require("request"); 114 | 115 | // Post message 116 | let options = { 117 | method: 'POST', 118 | url: 'https://api.ciscospark.com/v1/webhooks/incoming/' + webhook_id, 119 | headers: { 'Content-Type': 'application/json' }, 120 | body: { markdown: msg }, 121 | json: true 122 | }; 123 | 124 | request(options, function (err, response, body) { 125 | if (err) { 126 | if (cb) cb(err.message); 127 | return; 128 | } 129 | 130 | if (response.statusCode == 204) { 131 | console.log("message pushed to Webex Teams"); 132 | if (cb) cb(null); 133 | return; 134 | } 135 | 136 | if (cb) cb("Could not post message to Webex Teams"); 137 | }); 138 | } 139 | -------------------------------------------------------------------------------- /controls/agenda_notif/img/incoming_webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/agenda_notif/img/incoming_webhook.png -------------------------------------------------------------------------------- /controls/agenda_notif/img/push_to_teams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/agenda_notif/img/push_to_teams.png -------------------------------------------------------------------------------- /controls/agenda_notif/img/touch10_snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/agenda_notif/img/touch10_snapshot.png -------------------------------------------------------------------------------- /controls/agenda_notif/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agenda_notif", 3 | "version": "0.1.0", 4 | "description": "In-Room Control to display an list of session, and push to a Webex teams.", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "Stève Sfartz ", 9 | "license": "MIT", 10 | "dependencies": { 11 | "jsxapi": "^4.1.3", 12 | "request": "^2.87.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /controls/agenda_notif/util/sessions.js: -------------------------------------------------------------------------------- 1 | // 2 | // Utilities: catalog of sessions 3 | // 4 | 5 | // List of sessions 6 | const sessions = {}; 7 | sessions["steve"] = { 8 | id: "push_steve", 9 | title: "KeyNote - When apps meet infrastructure", 10 | description: "...", 11 | location: "Room 1", 12 | type: "keynote", 13 | day: "Thursday", 14 | time: "09:30AM", 15 | duration: "15", 16 | speaker: "Stève Sfartz" 17 | } 18 | 19 | sessions["matt"] = { 20 | id: "push_matt", 21 | title: "My developer journey towards true hybrid cloud with Kubernetes", 22 | description: "...", 23 | location: "Room 6", 24 | type: "technical talk", 25 | day: "Thursday", 26 | time: "11:30AM", 27 | duration: "40", 28 | speaker: "Matt Johnson" 29 | } 30 | 31 | sessions["roger"] = { 32 | id: "push_roger", 33 | title: "Making Enterprise Virtual Reality a Practical Reality", 34 | description: "...", 35 | location: "Room 6", 36 | type: "technical talk", 37 | day: "Thursday", 38 | time: "12:30PM", 39 | duration: "40", 40 | 41 | } 42 | 43 | sessions["cory"] = { 44 | id: "push_cory", 45 | title: "API Magic and Applications on the Network [Cory Guynn]", 46 | description: "...", 47 | location: "Room 6", 48 | type: "technical talk", 49 | day: "Thursday", 50 | time: "2:10PM", 51 | duration: "4O" 52 | } 53 | 54 | sessions["challenge"] = { 55 | id: "push_challenge", 56 | title: "Grab the Bag Challenge & Demos", 57 | description: "...", 58 | location: "Cisco Booth", 59 | type: "activity", 60 | day: "Thursday/Friday", 61 | time: "Till 4:00PM", 62 | duration: "300" 63 | } 64 | 65 | sessions["labs"] = { 66 | id: "push_labs", 67 | title: "Discover Kubernetes, Meraki and Webex Devices", 68 | description: "...", 69 | location: "Hands-On Labs", 70 | type: "activity", 71 | day: "Thursday", 72 | time: "11:30AM", 73 | duration: "360" 74 | } 75 | 76 | sessions["demos"] = { 77 | id: "push_demos", 78 | title: "Meet Cisco DevNet: Meraki & Webex Devices Demos", 79 | description: "...", 80 | location: "Cisco Booth", 81 | type: "activity", 82 | day: "Thursday/Friday", 83 | time: "Til 6:00PM", 84 | duration: "360" 85 | } 86 | 87 | 88 | // Export function 89 | module.exports = function getInfo(sessionId) { 90 | return sessions[sessionId]; 91 | } 92 | -------------------------------------------------------------------------------- /controls/agenda_post/README.md: -------------------------------------------------------------------------------- 1 | # Agenda with notifications (via Bot Account) 2 | 3 | This agenda pushes session details to a Webex Teams space, via a Bot Account posting to the space. 4 | 5 | ![](img/touch10_snapshot.png) 6 | 7 | 8 | ## Quickstart 9 | 10 | Deploy the [control](./agenda.xml) to your device 11 | 12 | Create a [bot account](https://apphub.webex.com/categories/other/integrations/incoming-webhooks-cisco-systems) from Webex for Developers, and paste the bot token into the code below. 13 | 14 | Run the JS script. 15 | 16 | ```shell 17 | git clone https://github.com/ObjectIsAdvantag/xapi-samples 18 | cd controls 19 | cd agenda_post 20 | npm install 21 | JSXAPI_DEVICE_URL='ssh://192.168.1.34' JSXAPI_USERNAME='integrator' JSXAPI_PASSWORD='integrator' node agenda.js 22 | ``` 23 | 24 | Press 'push' and see messages poping up in Webex Teams. 25 | 26 | ![](img/push_to_teams.png) -------------------------------------------------------------------------------- /controls/agenda_post/img/push_to_teams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/agenda_post/img/push_to_teams.png -------------------------------------------------------------------------------- /controls/agenda_post/img/touch10_snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/agenda_post/img/touch10_snapshot.png -------------------------------------------------------------------------------- /controls/agenda_post/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agenda_notif", 3 | "version": "0.1.0", 4 | "description": "In-Room Control to display an list of session, and push to a Webex teams.", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "Stève Sfartz ", 9 | "license": "MIT", 10 | "dependencies": { 11 | "jsxapi": "^4.2.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /controls/family_link/README.md: -------------------------------------------------------------------------------- 1 | # Intensive Care Unit - Family Link 2 | 3 | The WebexQuickDial XML menu presents a simple way for clinical staff to start a Personal Meeting Room for a bedspace (below showing ICU-101 -> ICU-108) 4 | 5 | ![](docs/ICU-DX.png) 6 | 7 | 8 | The WebexQuickDial.js file initiates a call into Personal Meeting Room related to individual buttons: 9 | 10 | - MATCH_STRING: uses the value entered in the XML file for the respective button 11 | - const DIALPREFIX_WEBEXURL: this value is replaced with the text on the button control after wqd- and will use it to join the actual PMR 12 | - const DIALPREPOSTFIX_WEBEXURL: this is the customer’s webex site 13 | - const DIALPOSTFIX_WEBEXURL: '@webex.com'; added after the customer’s site 14 | - let hostpin = '1234'; this needs to change, and reflect the actual Host PIN configured for the PMR 15 | -------------------------------------------------------------------------------- /controls/family_link/WebexQuickDial.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | // by Peter Welburn (petwelbu@cisco.com) 6 | // 7 | 8 | const xapi = require('xapi'); 9 | 10 | // Set this is you don't want to prompt users for a PIN 11 | let hostpin = '1234'; 12 | 13 | // This is what every widget on the UI should have as a prefix for us to make a webex call 14 | // REPLACE with your UI Extensions 15 | const MATCH_STRING = 'wqd-'; 16 | 17 | // REPLACE with values for your Webex Devicces deployment 18 | const DIALPREFIX_WEBEXURL = 'devnet'; 19 | const DIALPREPOSTFIX_WEBEXURL = '.devnetzone'; 20 | const DIALPOSTFIX_WEBEXURL = '@webex.com'; 21 | 22 | const KEYBOARD_TYPES = { 23 | NUMERIC : 'Numeric' 24 | , SINGLELINE : 'SingleLine' 25 | , PASSWORD : 'Password' 26 | , PIN : 'PIN' 27 | } 28 | const CALL_TYPES = { 29 | AUDIO : 'Audio' 30 | , VIDEO : 'Video' 31 | } 32 | 33 | const DIALPAD_ID = 'webexdialpad'; 34 | const DIALHOSTPIN_ID = 'webexhostpin'; 35 | 36 | const INROOMCONTROL_WEBEXCONTROL_PANELID = 'webexdialler'; 37 | const INROOMCONTROL_WEBEXQUICKDIAL_PANELID = 'webexquickdial'; 38 | 39 | /* Use these to check that its a valid number (depending on what you want to allow users to call */ 40 | const REGEXP_URLDIALER = /([a-zA-Z0-9@_\-\.]+)/; /* . Use this one if you want to allow URL dialling */ 41 | const REGEXP_NUMERICDIALER = /^([0-9]{1,2})$/; /* Use this one if you want to limit calls to numeric only. In this example, require number to be between 3 and 10 digits. */ 42 | 43 | let webexnumbertodial = ''; 44 | let isInWebexCall = 0; 45 | 46 | function sleep(ms) { 47 | return new Promise(resolve => setTimeout(resolve, ms)); 48 | } 49 | 50 | xapi.event.on('CallDisconnect', (event) => { 51 | isInWebexCall = 0; 52 | }); 53 | 54 | function showDialPad(text){ 55 | xapi.command("UserInterface Message TextInput Display", { 56 | InputType: KEYBOARD_TYPES.NUMERIC 57 | , Placeholder: '1 or 2-digit room number' 58 | , Title: "Webex Call" 59 | , Text: text 60 | , SubmitText: "Next" 61 | , FeedbackId: DIALPAD_ID 62 | }).catch((error) => { console.error(error); }); 63 | } 64 | 65 | /* This is the listener for the in-room control panel button that will trigger the dial panel to appear */ 66 | xapi.event.on('UserInterface Extensions Panel Clicked', (event) => { 67 | if (event.PanelId === INROOMCONTROL_WEBEXCONTROL_PANELID){ 68 | showDialPad("Enter the 1 or 2-digit room number:" ); 69 | } 70 | }); 71 | 72 | xapi.event.on('UserInterface Extensions Widget Action', (event) => { 73 | if (event.WidgetId.search(MATCH_STRING) !== -1) { 74 | webexnumbertodial = event.WidgetId.replace("wqd-", "") 75 | console.log(webexnumbertodial); 76 | webexnumbertodial = webexnumbertodial + DIALPREPOSTFIX_WEBEXURL + DIALPOSTFIX_WEBEXURL ; 77 | xapi.command("dial", {Number: webexnumbertodial}).catch((error) => { console.error(error); }); 78 | } 79 | }); 80 | 81 | xapi.event.on('UserInterface Message TextInput Response', (event) => { 82 | switch(event.FeedbackId) { 83 | case DIALPAD_ID: 84 | let regex = REGEXP_NUMERICDIALER; // First check, is it a valid number to dial 85 | let match = regex.exec(event.Text); 86 | if (match !== null) { 87 | let contains_at_regex = /@/; 88 | let contains_at_in_dialstring = contains_at_regex.exec(event.Text); 89 | if (contains_at_in_dialstring !== null) { 90 | webexnumbertodial = match[1]; 91 | } else { 92 | webexnumbertodial = match[1]; 93 | webexnumbertodial = DIALPREFIX_WEBEXURL + webexnumbertodial + DIALPREPOSTFIX_WEBEXURL + DIALPOSTFIX_WEBEXURL ; // Here we add the default hostname to the SIP number 94 | } 95 | if (hostpin === '') { 96 | sleep(200).then(() => { //this is a necessary trick to get it working with multiple touch panels to not mess up event-clears from other panels 97 | xapi.command("UserInterface Message TextInput Display", { 98 | InputType: KEYBOARD_TYPES.NUMERIC 99 | , Placeholder: "Hostpin (optional)" 100 | , Title: "Enter Host pin or leave blank" 101 | , Text: 'Webex call number:' + webexnumbertodial 102 | , SubmitText: "Dial" 103 | , FeedbackId: DIALHOSTPIN_ID 104 | }).catch((error) => { console.error(error); }); 105 | }); 106 | } else { 107 | xapi.command("dial", {Number: webexnumbertodial}).catch((error) => { console.error(error); }); 108 | } 109 | } else { 110 | showDialPad("You typed in an invalid number. Enter a 1 or 2-digit number." ); 111 | } 112 | break; 113 | 114 | case DIALHOSTPIN_ID: 115 | hostpin = event.Text; 116 | xapi.command("dial", {Number: webexnumbertodial}).catch((error) => { console.error(error); }); 117 | break; 118 | } 119 | }); 120 | 121 | xapi.status.on('Conference Call', (callInfo) => { 122 | if (callInfo.AuthenticationRequest === 'HostPinOrGuest') { 123 | isInWebexCall = 1; 124 | console.log(callInfo); 125 | xapi.command('Conference Call AuthenticationResponse', { 126 | CallId: callInfo.id, 127 | ParticipantRole: 'Host', 128 | Pin: hostpin + '#', 129 | }); 130 | } 131 | }); -------------------------------------------------------------------------------- /controls/family_link/WebexQuickDial.xml: -------------------------------------------------------------------------------- 1 | 2 | 1.7 3 | 4 | 1 5 | webexquickdial 6 | local 7 | Home 8 | Webex 9 | #FF7033 10 | Join Webex 11 | Custom 12 | 13 | ICU Webex 14 | 15 | Row 16 | 17 | wqd-icu-1 18 | ICU-1 19 | Button 20 | size=2 21 | 22 | 23 | wqd-icu-2 24 | ICU-2 25 | Button 26 | size=2 27 | 28 | 29 | wqd-icu-3 30 | ICU-3 31 | Button 32 | size=2 33 | 34 | 35 | wqd-icu-4 36 | ICU-4 37 | Button 38 | size=2 39 | 40 | 41 | wqd-icu-5 42 | ICU-5 43 | Button 44 | size=2 45 | 46 | 47 | wqd-icu-6 48 | ICU-6 49 | Button 50 | size=2 51 | 52 | 53 | wqd-icu-7 54 | ICU-7 55 | Button 56 | size=2 57 | 58 | 59 | wqd-icu-8 60 | ICU-8 61 | Button 62 | size=2 63 | 64 | 65 | wqd-icu-9 66 | ICU-9 67 | Button 68 | size=2 69 | 70 | 71 | wqd-icu-10 72 | ICU-10 73 | Button 74 | size=2 75 | 76 | 77 | 78 | Row 79 | 80 | wqd-icuisolation-1 81 | ICU Isolation 1 82 | Button 83 | size=2 84 | 85 | 86 | wqd-icuisolation-2 87 | ICU Isolation 2 88 | Button 89 | size=2 90 | 91 | 92 | hideRowNames=1 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /controls/family_link/docs/ICU-DX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/family_link/docs/ICU-DX.png -------------------------------------------------------------------------------- /controls/hue/README.md: -------------------------------------------------------------------------------- 1 | # Philips Hue demo 2 | 3 | Control a Philips Hue Light from a Cisco Collaboration Device. 4 | 5 | Steps: 6 | - Open the [macro](./macro.js) and update BRIDGE_IP, BRIDGE_USER with your deployment info 7 | - Load the macro and the panel to your CE9.6+ capable device 8 | 9 | 10 | ![control](./docs/control.png) 11 | 12 | 13 | ![light](./docs/light.png) 14 | -------------------------------------------------------------------------------- /controls/hue/control.xml: -------------------------------------------------------------------------------- 1 | 2 | 1.5 3 | 4 | Hue 5 | Statusbar 6 | Sliders 7 | 2 8 | #A866FF 9 | Philips Hue 10 | 11 | Basic 12 | 13 | Switch 14 | 15 | toggle 16 | ToggleButton 17 | size=1 18 | 19 | 20 | widget_22 21 | 22 | Text 23 | size=3;fontSize=normal;align=center 24 | 25 | 26 | 27 | Pick a color 28 | 29 | color_group 30 | GroupButton 31 | size=4 32 | 33 | 34 | color_red 35 | Red 36 | 37 | 38 | color_green 39 | Green 40 | 41 | 42 | color_blue 43 | Blue 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Advanced 52 | 53 | Saturation 54 | 55 | widget_19 56 | Slider 57 | size=4 58 | 59 | 60 | 61 | Brightness 62 | 63 | widget_20 64 | Slider 65 | size=4 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /controls/hue/docs/control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/hue/docs/control.png -------------------------------------------------------------------------------- /controls/hue/docs/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/hue/docs/light.png -------------------------------------------------------------------------------- /controls/hue/macro.js: -------------------------------------------------------------------------------- 1 | const xapi = require('xapi') 2 | 3 | 4 | // Update for your Hue deployment 5 | const BRIDGE_IP = '192.168.1.33' 6 | const BRIDGE_USER = 'SECRET' 7 | const LIGHT_ID = 1 // number of your Bulb as registered at your Hue Bridge 8 | 9 | 10 | // 11 | // Interact with Hue Lights 12 | // 13 | 14 | const COLOR_RED = 65535 15 | const COLOR_BLUE = 46920 16 | const COLOR_GREEN = 25500 17 | 18 | function changeColorForLight(light, color) { 19 | console.debug(`changeColor: ${color} ForLight: ${light}`) 20 | updateLight(BRIDGE_IP, BRIDGE_USER, light, { "hue": color }, console.log) 21 | } 22 | 23 | function toggleLight(light, bool) { 24 | console.debug(`toggleLight: ${light} to: ${bool}`) 25 | updateLight(BRIDGE_IP, BRIDGE_USER, light, { "on": bool }, console.log) 26 | } 27 | 28 | function updateLight(bridgeip, username, light, payload, cb) { 29 | console.debug('updateLight: pushing payload') 30 | console.debug(`bridgeip: ${bridgeip} light: ${light} payload: ${JSON.stringify(payload)}`) 31 | 32 | // Post message 33 | xapi.command( 34 | 'HttpClient Put', 35 | { 36 | Header: ["Content-Type: application/json"], 37 | Url: `http://${bridgeip}/api/${username}/lights/${light}/state` 38 | }, 39 | JSON.stringify(payload)) 40 | .then((response) => { 41 | if (response.StatusCode == 200) { 42 | console.log("message pushed to bridge") 43 | if (cb) cb(null, response.StatusCode) 44 | return 45 | } 46 | 47 | console.warn("updateLight: request failed with status code: " + response.StatusCode) 48 | if (cb) cb("failed with status code: " + response.StatusCode, response.StatusCode) 49 | }) 50 | .catch((err) => { 51 | console.error("updateLight: failed with err: " + err.message) 52 | if (cb) cb("Could not contact the bridge") 53 | }) 54 | } 55 | 56 | 57 | // Listen to In-Room control events 58 | xapi.event.on('UserInterface Extensions Widget Action', (action) => { 59 | console.log(`new event from group: ${action.WidgetId}`) 60 | 61 | // Toggle (on/off) 62 | if ((action.WidgetId === 'toggle') && (action.Type === 'changed')) { 63 | console.info(`toggling light to: ${action.Value}`) 64 | 65 | // [WORKAROUND] Switch Light and UI to Red as we cannot read from the macro 66 | changeColorForLight(LIGHT_ID, COLOR_RED) 67 | xapi.command('UserInterface Extensions Widget SetValue', { 68 | WidgetId: 'color_group', 69 | Value: 'color_red' 70 | }) 71 | 72 | // Switch on 73 | toggleLight(LIGHT_ID, (action.Value === 'on')) 74 | return 75 | } 76 | 77 | // Color 78 | if (action.WidgetId === 'color_group') { 79 | console.info(`color change requested by: ${action.Value}`) 80 | switch (action.Value) { 81 | case 'color_red': 82 | changeColorForLight(LIGHT_ID, COLOR_RED) 83 | break 84 | case 'color_blue': 85 | changeColorForLight(LIGHT_ID, COLOR_BLUE) 86 | break 87 | case 'color_green': 88 | changeColorForLight(LIGHT_ID, COLOR_GREEN) 89 | break 90 | default: 91 | console.log(`WARNING: unsupported color change request: suspected wrong widget id: ${action.Value}`) 92 | break 93 | } 94 | return 95 | } 96 | }) 97 | 98 | // Init at launch 99 | xapi.event.on('UserInterface Extensions Panel Clicked PanelId', (panel) => { 100 | console.debug(`new panel opened group: ${panel}`) 101 | 102 | // Toggle (on/off) 103 | if (panel === 'Hue') { 104 | console.log(`resetting panel: turning off, and color to red`) 105 | 106 | // update lights 107 | changeColorForLight(LIGHT_ID, COLOR_RED) 108 | toggleLight(LIGHT_ID, false) 109 | 110 | // update UI 111 | xapi.command('UserInterface Extensions Widget SetValue', { 112 | WidgetId: 'color_group', 113 | Value: 'color_red' 114 | }) 115 | xapi.command('UserInterface Extensions Widget SetValue', { 116 | WidgetId: 'toggle', 117 | Value: 'Off' 118 | }) 119 | 120 | return 121 | } 122 | }) 123 | 124 | 125 | console.info('Philips Hue macro listening...') 126 | -------------------------------------------------------------------------------- /controls/hue/updateLight.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Posts a payload to the specified light number of a Hue Bridge 3 | * 4 | */ 5 | 6 | const xapi = require('xapi'); 7 | 8 | function updateLight(bridgeip, username, light, payload, cb) { 9 | 10 | // Post message 11 | xapi.command( 12 | 'HttpClient Put', 13 | { 14 | Header: ["Content-Type: application/json"], 15 | Url: `http://${bridgeip}/api/${username}/lights/${light}/state` 16 | }, 17 | JSON.stringify(payload)) 18 | .then((response) => { 19 | if (response.StatusCode == 200) { 20 | console.log("message pushed to bridge") 21 | if (cb) cb(null, response.StatusCode) 22 | return 23 | } 24 | 25 | console.log("failed with status code: " + response.StatusCode) 26 | if (cb) cb("failed with status code: " + response.StatusCode, response.StatusCode) 27 | }) 28 | .catch((err) => { 29 | console.log("failed with err: " + err.message) 30 | if (cb) cb("Could not contact the bridge") 31 | }) 32 | } 33 | 34 | // Update for your Hue deployment 35 | const BRIDGE_IP = '192.168.1.33' 36 | const BRIDGE_USER = 'SECRET' 37 | const LIGHT_ID = 1 // number of your Bulb as registered at your Hue Bridge 38 | 39 | // Turn green color 40 | updateLight(BRIDGE_IP, BRIDGE_USER, LIGHT_ID, { "hue": 25500 }, console.log) 41 | -------------------------------------------------------------------------------- /controls/hue_disco/README.md: -------------------------------------------------------------------------------- 1 | # Turn your meeting room into a Hue Disco 2 | 3 | Configure and deploy the macro 4 | 5 | Deploy the UI Extension 6 | 7 | Play disco! 8 | -------------------------------------------------------------------------------- /controls/hue_disco/control.xml: -------------------------------------------------------------------------------- 1 | 2 | 1.6 3 | 4 | DISCO 5 | Home 6 | Lightbulb 7 | 1 8 | #FFB400 9 | Disco 10 | Custom 11 | 12 | Settings 13 | 14 | Blink 15 | 16 | DISCO_slider 17 | Slider 18 | size=4 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /controls/hue_disco/macro.js: -------------------------------------------------------------------------------- 1 | const xapi = require('xapi'); 2 | 3 | xapi.on('ready', init); 4 | 5 | function init() { 6 | let slider = 0; 7 | xapi.event.on('UserInterface Extensions Widget Action', (event) => { 8 | if ((event.WidgetId == 'DISCO_slider') && (event.Type == 'released')) { 9 | console.debug(`updated slider to: ${event.Value}`); 10 | slider = event.Value; 11 | } 12 | }); 13 | xapi.command('UserInterface Extensions Widget SetValue', { 14 | WidgetId: 'DISCO_slider', 15 | Value: 0 16 | }) 17 | .catch((err) => { 18 | if (err.message == 'Unknown widget: \'DISCO_slider\'') { 19 | console.warn('is the Disco Control Panel deployed? no worries I can live with it...'); 20 | } 21 | }) 22 | 23 | const MIN_DELAY = 300; 24 | function latestDelay(max) { 25 | let delay = Math.round((255 - slider) / 255 * max) + MIN_DELAY; 26 | console.debug(`new delay: ${delay}`) 27 | return delay; 28 | } 29 | 30 | disco(latestDelay, 2200, false); 31 | } 32 | 33 | 34 | // Blinks with random colors with a delay that can be dynamically changed 35 | // example: disco(latestDelay, 3000, false); 36 | 37 | function disco(getDelay, maxDelay, black) { 38 | // Get current value for delay 39 | let delay = getDelay(maxDelay); 40 | 41 | // Randomize color 42 | let color = COLOR_BLACK; 43 | if (delay < 800 || !black) { 44 | color = { 45 | hue: Math.round(Math.random() * 65535), 46 | on: true, 47 | sat: 255 48 | }; 49 | 50 | } 51 | black = !black; 52 | changeColor(color); 53 | 54 | // Randomize delay 55 | setTimeout(function () { 56 | disco(getDelay, maxDelay, black); 57 | }, delay); 58 | } 59 | 60 | 61 | // 62 | // Hue Library 63 | // 64 | 65 | // ACTION: Configure for your Philips Hue deployment 66 | const HUE_BRIDGE = '192.168.1.33'; 67 | const HUE_USERNAME = 'EM2Vg2GtNUqAASukv47wm1pWY0FayFe48D03f6Cb'; 68 | const HUE_LIGHT = 1; // number of the light in your deployment 69 | 70 | console.info(`Hue Bridge with ip: ${HUE_BRIDGE}, bulb: ${HUE_LIGHT}`); 71 | const hue_url = `http://${HUE_BRIDGE}/api/${HUE_USERNAME}/lights/${HUE_LIGHT}/state` 72 | 73 | const COLOR_RED = { on: true, hue: 65535, sat: 255 }; 74 | const COLOR_BLUE = { on: true, hue: 46920, sat: 255 }; 75 | const COLOR_GREEN = { on: true, hue: 25500, sat: 255 }; 76 | const COLOR_WHITE = { on: true, hue: 0, sat: 0 }; 77 | const COLOR_BLACK = { on: false }; 78 | 79 | function changeColor(color) { 80 | 81 | console.debug(`posting to hue bulb: ${JSON.stringify(color)}`) 82 | 83 | // Post message 84 | xapi.command( 85 | 'HttpClient Put', 86 | { 87 | Header: ["Content-Type: application/json"], 88 | Url: hue_url, 89 | AllowInsecureHTTPS: "True" 90 | }, 91 | JSON.stringify(color)) 92 | .catch((err) => { 93 | console.error('could not contact the Hue Bridge'); 94 | }); 95 | } 96 | -------------------------------------------------------------------------------- /controls/maze/README.md: -------------------------------------------------------------------------------- 1 | # Play an interactive game 2 | 3 | **New to Controls & Macros? check the [QuickStart Guide](../../docs/QuickStart.md) to learn to load Controls and Macros to your device** 4 | 5 | 6 | Navigate blind in a maze, look for the treasure. 7 | 8 | ![](./maze.png) 9 | 10 | 11 | Hit 'Help' to check your current position against the map. 12 | 13 | ![](./map.png) 14 | 15 | 16 | Check these more advanced versions of the game with [a scoring system](../maze_scores) and [difficulty levels](../maze_levels). 17 | -------------------------------------------------------------------------------- /controls/maze/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/maze/map.png -------------------------------------------------------------------------------- /controls/maze/maze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/maze/maze.png -------------------------------------------------------------------------------- /controls/maze/maze.xml: -------------------------------------------------------------------------------- 1 | 2 | 1.4 3 | 4 | Disc 5 | Home 6 | 7 | Play the Maze 8 | 9 | Instructions 10 | 11 | instructions 12 | Then here you are, lost in an hostile maze, looking for the treasure. Pick a direction... 13 | Text 14 | size=4;align=left;fontSize=normal 15 | 16 | 17 | 18 | Pick a direction 19 | 20 | directions 21 | help 22 | DirectionalPad 23 | size=4 24 | 25 | 26 | 27 | Restart 28 | 29 | restart 30 | play again 31 | Button 32 | size=2 33 | 34 | 35 | unused 36 | 37 | Text 38 | size=2 39 | 40 | 41 | PlayMaze 42 | hideRowNames=0 43 | 44 | Play 45 | 46 | -------------------------------------------------------------------------------- /controls/maze_french/README.md: -------------------------------------------------------------------------------- 1 | # Maze Game with Scoring & Levels 2 | 3 | **New to Controls & Macros? check the [QuickStart Guide](../../docs/QuickStart.md) to learn to load Controls and Macros to your device** 4 | 5 | 6 | French version of the Maze game with scoring system and three difficulty levels. 7 | 8 | ![](./game.png) 9 | 10 | 11 | Check the rules 12 | 13 | ![](./rules.png) 14 | 15 | 16 | Change difficulty level as you succeed 17 | 18 | ![](./options.png) -------------------------------------------------------------------------------- /controls/maze_french/game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/maze_french/game.png -------------------------------------------------------------------------------- /controls/maze_french/options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/maze_french/options.png -------------------------------------------------------------------------------- /controls/maze_french/rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/maze_french/rules.png -------------------------------------------------------------------------------- /controls/maze_levels/README.md: -------------------------------------------------------------------------------- 1 | # Maze Game with Scoring & Levels 2 | 3 | **New to Controls & Macros? check the [QuickStart Guide](../../docs/QuickStart.md) to learn to load Controls and Macros to your device** 4 | 5 | 6 | This version of the Maze game incorporates a scoring system doubled with several difficulty levels. 7 | 8 | ![](./options.png) 9 | 10 | 11 | Concretely, the maze will get bigger if you choose a more advanced level. 12 | 13 | ![](./map.png) -------------------------------------------------------------------------------- /controls/maze_levels/game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/maze_levels/game.png -------------------------------------------------------------------------------- /controls/maze_levels/levels.xml: -------------------------------------------------------------------------------- 1 | 2 | 1.4 3 | 4 | Disc 5 | Home 6 | 7 | Play the Maze 8 | 9 | Score 10 | 11 | score 12 | 13 | Text 14 | size=2;fontSize=normal 15 | 16 | 17 | restart 18 | restart 19 | Button 20 | size=2 21 | 22 | 23 | 24 | Instructions 25 | 26 | instructions 27 | Then here you are, lost in an hostile maze, looking for the treasure. Pick a direction... 28 | Text 29 | size=4;align=left;fontSize=normal 30 | 31 | 32 | 33 | Pick a direction 34 | 35 | directions 36 | help 37 | DirectionalPad 38 | size=4 39 | 40 | 41 | hideRowNames=0 42 | 43 | 44 | Rules 45 | 46 | Instructions 47 | 48 | rules 49 | Navigate around the maze and find the treasure. Press 'Help' to show the map. Note that it will cost you 500 points every time you ask for it. 50 | Text 51 | size=4;fontSize=small 52 | 53 | 54 | 55 | Symbols 56 | 57 | symbol_location 58 | 'o' : your current location 59 | Text 60 | size=2;fontSize=small 61 | 62 | 63 | symbol_treasure 64 | '?' : the treasure (+5.000) 65 | Text 66 | size=2;fontSize=small 67 | 68 | 69 | 70 | 71 | 72 | symbol_nothing 73 | '_' : nothing here (+50) 74 | Text 75 | size=2;fontSize=small 76 | 77 | 78 | symbol_border 79 | '|' : maze border (-200) 80 | Text 81 | size=2;fontSize=small 82 | 83 | 84 | 85 | 86 | 87 | symbol_wall 88 | 'X' : a wall (-100) 89 | Text 90 | size=2;fontSize=small 91 | 92 | 93 | symbol_cat 94 | 'C' : a cat (+200) 95 | Text 96 | size=2;fontSize=small 97 | 98 | 99 | 100 | 101 | 102 | symbol_dog 103 | 'D' : a dog (-200) 104 | Text 105 | size=2;fontSize=small 106 | 107 | 108 | symbol_monster 109 | 'M': a monster (-500) 110 | Text 111 | size=2;fontSize=small 112 | 113 | 114 | hideRowNames=0 115 | 116 | 117 | Options 118 | 119 | Level 120 | 121 | difficulty 122 | GroupButton 123 | size=4 124 | 125 | 126 | 0 127 | Rookie 128 | 129 | 130 | 1 131 | Seasoned 132 | 133 | 134 | 2 135 | Expert 136 | 137 | 138 | 139 | 140 | 141 | Version 142 | 143 | unused 144 | v1.0.0 145 | Text 146 | size=4;fontSize=normal;align=left 147 | 148 | 149 | 150 | About 151 | 152 | widget_1 153 | by @SteveSfartz (developer.cisco.com) 154 | Text 155 | size=4;fontSize=normal 156 | 157 | 158 | hideRowNames=0 159 | 160 | Play 161 | 162 | -------------------------------------------------------------------------------- /controls/maze_levels/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/maze_levels/map.png -------------------------------------------------------------------------------- /controls/maze_levels/options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/maze_levels/options.png -------------------------------------------------------------------------------- /controls/maze_levels/rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/maze_levels/rules.png -------------------------------------------------------------------------------- /controls/maze_scores/README.md: -------------------------------------------------------------------------------- 1 | # Maze Game with Scoring 2 | 3 | **New to Controls & Macros? check the [QuickStart Guide](../../docs/QuickStart.md) to learn to load Controls and Macros to your device** 4 | 5 | 6 | This version of the Maze game incorporates a scoring system: 7 | 8 | ![](./game.png) 9 | 10 | 11 | Moreover, the rules are exposed in a dedicated tab: 12 | 13 | ![](./rules.png) 14 | -------------------------------------------------------------------------------- /controls/maze_scores/game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/maze_scores/game.png -------------------------------------------------------------------------------- /controls/maze_scores/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maze_scores", 3 | "version": "0.1.0", 4 | "description": "Maze powered by a macro, with scoring system", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "Stève Sfartz ", 9 | "license": "MIT", 10 | "dependencies": { 11 | "jsxapi": "^4.1.3" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /controls/maze_scores/rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/maze_scores/rules.png -------------------------------------------------------------------------------- /controls/maze_scores/score.xml: -------------------------------------------------------------------------------- 1 |  2 | 1.4 3 | 4 | Disc 5 | Home 6 | 7 | Play the Maze 8 | 9 | Score 10 | 11 | score 12 | not started 13 | Text 14 | size=2;fontSize=normal 15 | 16 | 17 | restart 18 | restart 19 | Button 20 | size=2 21 | 22 | 23 | 24 | Instructions 25 | 26 | instructions 27 | Then here you are, lost in an hostile maze, looking for the treasure. Pick a direction... 28 | Text 29 | size=4;align=left;fontSize=normal 30 | 31 | 32 | 33 | Pick a direction 34 | 35 | directions 36 | help 37 | DirectionalPad 38 | size=4 39 | 40 | 41 | PlayMaze 42 | hideRowNames=0 43 | 44 | 45 | Rules 46 | 47 | Instructions 48 | 49 | rules 50 | Navigate around the maze and find the treasure. Press help to show the map, note that it will cost you 500 points every time you ask for it. 51 | Text 52 | size=4;fontSize=small 53 | 54 | 55 | 56 | Symbols 57 | 58 | symbol_location 59 | 'o' : your current location 60 | Text 61 | size=2;fontSize=small 62 | 63 | 64 | symbol_treasure 65 | '?' : the treasure (+5.000) 66 | Text 67 | size=2;fontSize=small 68 | 69 | 70 | 71 | 72 | 73 | symbol_nothing 74 | '_' : nothing here (+50) 75 | Text 76 | size=2;fontSize=small 77 | 78 | 79 | symbol_border 80 | '|' : maze border (-200) 81 | Text 82 | size=2;fontSize=small 83 | 84 | 85 | 86 | 87 | 88 | symbol_wall 89 | 'X' : a wall (-100) 90 | Text 91 | size=2;fontSize=small 92 | 93 | 94 | symbol_cat 95 | 'C' : a cat (+200) 96 | Text 97 | size=2;fontSize=small 98 | 99 | 100 | 101 | 102 | 103 | symbol_dog 104 | 'D' : a dog (-200) 105 | Text 106 | size=2;fontSize=small 107 | 108 | 109 | symbol_monster 110 | 'M': a monster (-500) 111 | Text 112 | size=2;fontSize=small 113 | 114 | 115 | 116 | About 117 | 118 | about 119 | by @SteveSfartz (https://developer.cisco.com) 120 | Text 121 | size=4;align=left;fontSize=small 122 | 123 | 124 | hideRowNames=0 125 | 126 | Play 127 | 128 | -------------------------------------------------------------------------------- /controls/notifier/README.md: -------------------------------------------------------------------------------- 1 | # Webex Teams Notifier 2 | 3 | In-Room Control that lets users set and update an email address, and sends Webex Teams notifications to this email via a bot. 4 | 5 | ![](img/panel_notify.png) 6 | 7 | ![](img/panel_recipient.png) 8 | 9 | 10 | ## Quickstart 11 | 12 | Deploy the [control](./notifier.xml) to your device 13 | 14 | Run the `notifier.js` script from a bash command line: 15 | 16 | ```shell 17 | git clone https://github.com/ObjectIsAdvantag/xapi-samples 18 | cd controls 19 | cd notifier 20 | npm install 21 | JSXAPI_DEVICE_URL='ssh://192.168.1.34' JSXAPI_USERNAME='integrator' JSXAPI_PASSWORD='integrator' node notifier.js 22 | ``` 23 | 24 | Press 'push' and see notifications poping up on your screen. 25 | 26 | ## Sending notifications to Webex Teams 27 | 28 | Create a [Webex Teams bot account](https://developer.webex.com/add-bot.html) and paste the bot access token. 29 | 30 | Run the `to_webex_teams.js` script from a bash command line: 31 | 32 | ```shell 33 | git clone https://github.com/ObjectIsAdvantag/xapi-samples 34 | cd controls 35 | cd notifier 36 | npm install 37 | ACCESS_TOKEN="XXXXX" JSXAPI_DEVICE_URL='ssh://192.168.1.34' JSXAPI_USERNAME='integrator' JSXAPI_PASSWORD='integrator' node to_webex_teams.js 38 | ``` 39 | 40 | Press 'push' and see messages poping up in the Webex Teams space. 41 | 42 | ![](img/push_to_teams.png) 43 | 44 | -------------------------------------------------------------------------------- /controls/notifier/img/panel_notify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/notifier/img/panel_notify.png -------------------------------------------------------------------------------- /controls/notifier/img/panel_recipient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/notifier/img/panel_recipient.png -------------------------------------------------------------------------------- /controls/notifier/img/push_to_teams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/notifier/img/push_to_teams.png -------------------------------------------------------------------------------- /controls/notifier/img/touch10_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/notifier/img/touch10_home.png -------------------------------------------------------------------------------- /controls/notifier/notifier.xml: -------------------------------------------------------------------------------- 1 |  2 | 1.5 3 | 4 | panel_3 5 | Statusbar 6 | Spark 7 | 2 8 | #D541D8 9 | Notifier 10 | 11 | Notify 12 | 13 | Where are you ? 14 | 15 | send_where_are_you 16 | send 17 | Button 18 | size=2 19 | 20 | 21 | widget_6 22 | 23 | Text 24 | size=2;fontSize=normal;align=center 25 | 26 | 27 | 28 | Meeting canceled 29 | 30 | send_meeting_canceled 31 | send 32 | Button 33 | size=2 34 | 35 | 36 | widget_7 37 | 38 | Text 39 | size=2;fontSize=normal;align=center 40 | 41 | 42 | 43 | What about coffee ? 44 | 45 | send_what_about_coffee 46 | send 47 | Button 48 | size=2 49 | 50 | 51 | widget_8 52 | 53 | Text 54 | size=2;fontSize=normal;align=center 55 | 56 | 57 | 58 | 59 | 60 | Recipient 61 | 62 | Webex Teams 63 | 64 | recipient_email 65 | please enter an email address 66 | Text 67 | size=4;fontSize=normal;align=left 68 | 69 | 70 | update_email 71 | update 72 | Button 73 | size=2 74 | 75 | 76 | reset_email 77 | reset 78 | Button 79 | size=2 80 | 81 | 82 | 83 | 84 | 85 | About 86 | 87 | Author 88 | 89 | widget_9 90 | Stève Sfartz <stsfartz@cisco.com> 91 | Text 92 | size=4;fontSize=small;align=left 93 | 94 | 95 | 96 | Resources 97 | 98 | widget_10 99 | https://github.com/ObjectIsAdvantag/xapi-samples 100 | Text 101 | size=4;fontSize=small;align=left 102 | 103 | 104 | 105 | Version 106 | 107 | status_version 108 | 109 | Text 110 | size=4;fontSize=small;align=left 111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /controls/notifier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notifier", 3 | "version": "0.1.0", 4 | "description": "In-Room Control that lets users set and update an email address, and sends Webex Teams notifications to this address via a bot.", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "Stève Sfartz ", 9 | "license": "MIT", 10 | "dependencies": { 11 | "jsxapi": "^4.1.3", 12 | "request": "^2.87.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /controls/onair/README.md: -------------------------------------------------------------------------------- 1 | # OnAir - CanIBugDad? 2 | 3 | an In-Room Control that toogles a Hue bulb color, depending on the room's state: free, occupied, busy, on air (call in progress) 4 | 5 | ![](img/onair_busy.png) 6 | 7 | 8 | 9 | ## Quickstart 10 | 11 | Deploy the [control](./onair.xml) to your device 12 | 13 | Open the file `script.js` if you're planning to run/test/debug, or the `macro.js` if ready to deploy as a Macro. 14 | 15 | > Note: the `multi.js` file targets both the Macro runtime AND standalone Node.js execution. 16 | 17 | Update wih your Philipps Hue deployment settings. 18 | 19 | Example to run as the script from a shell terminal: 20 | 21 | ```shell 22 | git clone https://github.com/ObjectIsAdvantag/xapi-samples 23 | cd controls 24 | cd onair 25 | npm install 26 | JSXAPI_DEVICE_URL='ssh://192.168.1.32' JSXAPI_USERNAME='localadmin' JSXAPI_PASSWORD='ciscopsdt' node script.js 27 | ``` 28 | -------------------------------------------------------------------------------- /controls/onair/control.xml: -------------------------------------------------------------------------------- 1 | 2 | 1.5 3 | 4 | ONAIR 5 | Statusbar 6 | Lightbulb 7 | 2 8 | #FF503C 9 | OnAir 10 | 11 | Console 12 | 13 | 'OnAir' Status 14 | 15 | onair_console_statusIcon 16 | Button 17 | size=1;icon=blue 18 | 19 | 20 | onair_console_statusText 21 | then, where's dad ? 22 | Text 23 | size=3;fontSize=normal;align=center 24 | 25 | 26 | 27 | Mode 28 | 29 | onair_console_modeToggle 30 | ToggleButton 31 | size=1 32 | 33 | 34 | onair_console_modeText 35 | Automatic 36 | Text 37 | size=3;fontSize=normal;align=left 38 | 39 | 40 | 41 | Manual 42 | 43 | onair_console_manualGroup 44 | GroupButton 45 | size=4 46 | 47 | 48 | free 49 | Off 50 | 51 | 52 | occupied 53 | Free 54 | 55 | 56 | busy 57 | Busy 58 | 59 | 60 | onair 61 | OnAir 62 | 63 | 64 | 65 | 66 | onair_consolePanel 67 | 68 | 69 | 70 | Settings 71 | 72 | Switch 73 | 74 | widget_36 75 | ToggleButton 76 | size=1 77 | 78 | 79 | widget_37 80 | on/off 81 | Text 82 | size=3;fontSize=normal;align=left 83 | 84 | 85 | 86 | Automatic Mode 87 | 88 | widget_38 89 | Spacer 90 | size=4 91 | 92 | 93 | 94 | Standby delay 95 | 96 | widget_24 97 | 1 minute 98 | Text 99 | size=4;fontSize=normal;align=left 100 | 101 | 102 | 103 | Wakeup on Motion 104 | 105 | widget_25 106 | yes 107 | Text 108 | size=4;fontSize=normal;align=left 109 | 110 | 111 | 112 | DoNotDisturb Timeout 113 | 114 | widget_26 115 | 120 minutes 116 | Text 117 | size=4;fontSize=normal;align=left 118 | 119 | 120 | onair_settingsPanel 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /controls/onair/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "onair", 3 | "version": "0.1.1", 4 | "description": "an In-Room Control that toogles a Hue bulb color, depending on the room's state.", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "Stève Sfartz ", 9 | "license": "MIT", 10 | "dependencies": { 11 | "jsxapi": "^4.3.1", 12 | "request": "^2.88.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /controls/proximity/README.md: -------------------------------------------------------------------------------- 1 | # Helper panel to switch Proximity Mode on/off 2 | 3 | **New to Controls & Macros? check the [QuickStart Guide](../../docs/QuickStart.md) to learn to load Controls and Macros to your device** 4 | 5 | 6 | Simply press the toggle: 7 | 8 | ![](./proximity.png) 9 | 10 | 11 | Note that the toggle will [automatically update](https://github.com/ObjectIsAdvantag/xapi-samples/blob/master/controls/proximity/proximity.js#L52) if the configuration is updated from the Room Device Admin UI. 12 | -------------------------------------------------------------------------------- /controls/proximity/proximity.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * Macro companion to the Ultrasound Control 8 | * - lets users toggle Proximity Mode to On/Off 9 | * - displays the current MaxVolume level 10 | */ 11 | 12 | const xapi = require('xapi') 13 | 14 | 15 | // Change proximity mode to "On" or "Off" 16 | function switchProximityMode(mode) { 17 | console.debug(`switching proximity mode to: ${mode}`) 18 | 19 | xapi.config.set('Proximity Mode', mode) 20 | .then(() => { 21 | console.info(`turned proximity mode: ${mode}`) 22 | }) 23 | .catch((err) => { 24 | console.error(`could not turn proximity mode: ${mode}`) 25 | }) 26 | } 27 | 28 | // React to UI events 29 | function onGui(event) { 30 | // Proximity Mode Switch 31 | if ((event.Type == 'changed') && (event.WidgetId == 'proximity_switch')) { 32 | switchProximityMode(event.Value) 33 | return; 34 | } 35 | } 36 | xapi.event.on('UserInterface Extensions Widget Action', onGui); 37 | 38 | 39 | // 40 | // Proximity Services Availability 41 | // 42 | 43 | // Update Toogle if proximity mode changes 44 | function updateProximityToggle(mode) { 45 | console.debug(`switching toggle to ${mode}`) 46 | 47 | xapi.command("UserInterface Extensions Widget SetValue", { 48 | WidgetId: "proximity_switch", 49 | Value: mode 50 | }) 51 | } 52 | xapi.config.on("Proximity Mode", mode => { 53 | console.log(`proximity mode changed to: ${mode}`) 54 | 55 | // Update toggle 56 | // [WORKAROUND] Configuration is On or Off, needs to be turned to lowercase 57 | updateProximityToggle(mode.toLowerCase()) 58 | }) 59 | 60 | // Refresh Toggle state 61 | function refreshProximityToggle() { 62 | xapi.status.get("Proximity Services Availability") 63 | .then(availability => { 64 | console.debug(`current proximity mode is ${availability}`) 65 | switch (availability) { 66 | case 'Available': 67 | updateProximityToggle('on') 68 | return; 69 | 70 | case 'Disabled': 71 | default: 72 | updateProximityToggle('off') 73 | return; 74 | } 75 | }) 76 | .catch((err) => { 77 | console.error(`could not read current proximity mode, err: ${err.message}`) 78 | }) 79 | } 80 | 81 | // 82 | // Audio Ultrasound MaxVolume 83 | // 84 | function updateUltrasoundMaxVolume(volume) { 85 | console.debug(`updating Ultrasound text to: ${volume}`) 86 | 87 | xapi.command("UserInterface Extensions Widget SetValue", { 88 | WidgetId: "ultrasound_maxvolume", 89 | Value: volume 90 | }) 91 | } 92 | xapi.config.on("Audio Ultrasound MaxVolume", volume => { 93 | console.log(`Ultrasound maxVolume changed to: ${volume}`) 94 | 95 | // Update toggle 96 | // [WORKAROUND] Configuration is On or Off, needs to be turned to lowercase 97 | updateUltrasoundMaxVolume(volume) 98 | }) 99 | 100 | function refreshUltrasoundMaxVolume() { 101 | xapi.config.get("Audio Ultrasound MaxVolume") 102 | .then(volume => updateUltrasoundMaxVolume(volume)) 103 | } 104 | 105 | 106 | // 107 | // Reset UI 108 | // 109 | 110 | // Initialize at macro startup 111 | function refreshUserInterface() { 112 | refreshProximityToggle() 113 | refreshUltrasoundMaxVolume() 114 | } 115 | refreshUserInterface() 116 | 117 | // Initialize at widget deployment 118 | xapi.event.on('UserInterface Extensions Widget LayoutUpdated', (event) => { 119 | console.debug("layout updated, let's refresh our toogle") 120 | refreshUserInterface() 121 | }); 122 | -------------------------------------------------------------------------------- /controls/proximity/proximity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/proximity/proximity.png -------------------------------------------------------------------------------- /controls/proximity/proximity.xml: -------------------------------------------------------------------------------- 1 |  2 | 1.4 3 | 4 | Microphone 5 | Statusbar 6 | 7 | UltraSound Configuration 8 | 9 | Proximity 10 | 11 | proximity_switch 12 | ToggleButton 13 | size=1 14 | 15 | 16 | unused 17 | 18 | Text 19 | size=3 20 | 21 | 22 | 23 | MaxVolume 24 | 25 | ultrasound_maxvolume 26 | 27 | Text 28 | size=1;fontSize=normal;align=center 29 | 30 | 31 | unused 32 | (0 to 90) 33 | Text 34 | size=3;align=left;fontSize=normal 35 | 36 | 37 | 38 | 39 | Configuration 40 | 41 | -------------------------------------------------------------------------------- /controls/ultrasound/README.md: -------------------------------------------------------------------------------- 1 | # Helper panel to update the Ultrasound MaxVolume 2 | 3 | **New to Controls & Macros? check the [QuickStart Guide](../../docs/QuickStart.md) to learn to load Controls and Macros to your device** 4 | 5 | Note that you can deploy the custom code logic for the Ultrasound panel: 6 | - [as a Macro](./ultrasound-macro.js) 7 | - [as a standalone Node.js script](./ultrasound-jsxapi.js). 8 | - or [as a script](./ultrasound-jsxapi.js) that can be used either as a Macro or a standalone script 9 | 10 | Simply move the slider around. 11 | 12 | ![](./ultrasound.png) 13 | 14 | 15 | _Note that a custom message is pushed to your device's Touch10/DX interface as Webex Teams pairing is enabled / disabled._ 16 | -------------------------------------------------------------------------------- /controls/ultrasound/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ultrasound", 3 | "version": "0.2.0", 4 | "description": "Backend code for the Ultrasound control", 5 | "main": "ultrasound.js", 6 | "scripts": { 7 | "start": "node ultrasound.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "Stève Sfartz ", 11 | "license": "MIT", 12 | "dependencies": { 13 | "jsxapi": "^5.0.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /controls/ultrasound/ultrasound-macro.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | 7 | const xapi = require('xapi') 8 | 9 | xapi.on('ready', init) 10 | 11 | 12 | // CE maximum volume for Ultrasound 13 | // note: Since CE 9.9, max is 70 across all devices 14 | //const MAX = 90 // for a DX80 15 | //const MAX = 70 // for a RoomKit 16 | const MAX = 70 17 | 18 | function init() { 19 | console.log("connexion successful") 20 | 21 | // Initialize the widgets 22 | xapi.config.get('Audio Ultrasound MaxVolume') 23 | .then(updateUI) 24 | 25 | // Update configuration from UI actions 26 | xapi.event.on('UserInterface Extensions Widget Action', (event) => { 27 | if (event.WidgetId !== 'US_volume_slider') return 28 | if (event.Type !== 'released') return 29 | 30 | // Update Ultrasound configuration 31 | const volume = Math.round(parseInt(event.Value) * MAX / 255) 32 | console.log(`updating Ultrasound configuration to: ${volume}`) 33 | xapi.config.set('Audio Ultrasound MaxVolume', volume) 34 | }) 35 | 36 | // Update UI from configuration changes 37 | xapi.config.on('Audio Ultrasound MaxVolume', updateUI) 38 | 39 | // Update if the controls is (re-)deployed 40 | xapi.event.on('UserInterface Extensions Widget LayoutUpdated', (event) => { 41 | console.log(`layout updated, let's refresh the widgets`) 42 | xapi.config.get('Audio Ultrasound MaxVolume') 43 | .then(updateUI) 44 | }) 45 | } 46 | 47 | 48 | function updateUI(volume) { 49 | console.log(`updating UI to new Ultrasound configuration: ${volume}`) 50 | 51 | // Update Widget: slider 52 | xapi.command('UserInterface Extensions Widget SetValue', { 53 | WidgetId: 'US_volume_text', 54 | Value: volume 55 | }) 56 | .catch((error) => { 57 | console.log("cannot update UI component 'Volume level'. Is the panel deployed?") 58 | return 59 | }) 60 | 61 | // Update Widget: slider 62 | let newVolume = parseInt(volume) 63 | const level = Math.round(newVolume * 255 / MAX) 64 | xapi.command('UserInterface Extensions Widget SetValue', { 65 | WidgetId: 'US_volume_slider', 66 | Value: level 67 | }) 68 | .catch((error) => { 69 | console.log("cannot update UI component 'Volume slider'. Is the panel deployed?") 70 | return 71 | }) 72 | 73 | // Update "Pairing" Invite on the Monitor 74 | updatePairingInvite(newVolume) 75 | } 76 | 77 | function updatePairingInvite(volume) { 78 | 79 | const MIN_LEVEL_TO_PAIR = 5 // note that this is an arbitrary value 80 | if (volume < MIN_LEVEL_TO_PAIR) { 81 | xapi.config.set('UserInterface CustomMessage', "/!\\ Pairing is disabled") 82 | return 83 | } 84 | 85 | // Pick the message that suits your device's registration mode 86 | xapi.status.get('Webex Status') 87 | .then((status) => { 88 | // If cloud-registered 89 | if (status == 'Registered') { 90 | xapi.config.set('UserInterface CustomMessage', "Tip: Launch Webex Teams to pair") 91 | } 92 | else { 93 | // If registered on-premises (VCS or CUCM ) 94 | xapi.config.set('UserInterface CustomMessage', "Tip: Pair from a Proximity client") 95 | } 96 | }) 97 | } 98 | -------------------------------------------------------------------------------- /controls/ultrasound/ultrasound.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | // Bootstraping sequence to support running either as a JS macro or as standalone Node.js 7 | let xapi; 8 | try { 9 | // Run as a Macro 10 | xapi = require('xapi') 11 | console.log("running as a macro") 12 | } 13 | catch (err) { 14 | if (err.code != "MODULE_NOT_FOUND") { 15 | console.log(`unexpected error code: ${err.code}, exiting...`) 16 | process.exit(1) 17 | } 18 | 19 | // Bind to a CE device 20 | console.log("running as a standalone app") 21 | xapi = connect(process.env.JSXAPI_DEVICE_URL, process.env.JSXAPI_USERNAME, process.env.JSXAPI_PASSWORD) 22 | } 23 | 24 | xapi.on('ready', init) 25 | 26 | 27 | function connect(url, username, password) { 28 | let jsxapi = require('jsxapi') 29 | 30 | // Check args 31 | if (!url || !username) { 32 | console.error("Please specify info to connect to your device as JSXAPI_DEVICE_URL, JSXAPI_USERNAME, JSXAPI_PASSWORD env variables") 33 | console.error("Bash example: JSXAPI_DEVICE_URL='ssh://192.168.1.34' JSXAPI_USERNAME='integrator' JSXAPI_PASSWORD='integrator' node ultrasound-jsxapi.js") 34 | process.exit(1) 35 | } 36 | 37 | // Empty passwords are supported 38 | password = password ? password : "" 39 | 40 | // Connect to the device 41 | console.log(`connecting to device with url: ${url}`) 42 | let xapi = jsxapi.connect(url, { 43 | username: username, 44 | password: password 45 | }) 46 | xapi.on('error', (err) => { 47 | switch (err) { 48 | case "client-socket": 49 | console.error("could not connect: invalid URL.") 50 | break 51 | 52 | case "client-authentication": 53 | console.error("could not connect: invalid credentials.") 54 | break 55 | 56 | case "client-timeout": 57 | console.error("could not connect: timeout.") 58 | break 59 | 60 | default: 61 | console.error(`encountered error: ${err}.`) 62 | break 63 | } 64 | 65 | console.log("exiting...") 66 | process.exit(1) 67 | }) 68 | 69 | return xapi 70 | } 71 | 72 | // 73 | // Code logic 74 | // 75 | 76 | // CE maximum volume for Ultrasound 77 | // note: Since CE 9.9, max is 70 across all devices 78 | //const MAX = 90 // for a DX80 79 | //const MAX = 70 // for a RoomKit 80 | const MAX = 70 81 | 82 | function init() { 83 | console.log("successfully binded to device") 84 | 85 | // Initialize the widgets 86 | xapi.config.get('Audio Ultrasound MaxVolume') 87 | .then(updateUI) 88 | 89 | // Update configuration from UI actions 90 | xapi.event.on('UserInterface Extensions Widget Action', (event) => { 91 | if (event.WidgetId !== 'US_volume_slider') return 92 | if (event.Type !== 'released') return 93 | 94 | // Update Ultrasound configuration 95 | const volume = Math.round(parseInt(event.Value) * MAX / 255) 96 | console.log(`updating Ultrasound configuration to: ${volume}`) 97 | xapi.config.set('Audio Ultrasound MaxVolume', volume) 98 | }) 99 | 100 | // Update UI from configuration changes 101 | xapi.config.on('Audio Ultrasound MaxVolume', updateUI) 102 | 103 | // Update if the controls is (re-)deployed 104 | xapi.event.on('UserInterface Extensions Widget LayoutUpdated', (event) => { 105 | console.log(`layout updated, let's refresh the widgets`) 106 | xapi.config.get('Audio Ultrasound MaxVolume') 107 | .then(updateUI) 108 | }) 109 | } 110 | 111 | function updateUI(volume) { 112 | console.log(`updating UI to new Ultrasound configuration: ${volume}`) 113 | 114 | // Update Widget: slider 115 | xapi.command('UserInterface Extensions Widget SetValue', { 116 | WidgetId: 'US_volume_text', 117 | Value: volume 118 | }) 119 | .catch((error) => { 120 | console.log("cannot update UI component 'Volume level'. Is the panel deployed?") 121 | return 122 | }) 123 | 124 | // Update Widget: slider 125 | let newVolume = parseInt(volume) 126 | const level = Math.round(newVolume * 255 / MAX) 127 | xapi.command('UserInterface Extensions Widget SetValue', { 128 | WidgetId: 'US_volume_slider', 129 | Value: level 130 | }) 131 | .catch((error) => { 132 | console.log("cannot update UI component 'Volume slider'. Is the panel deployed?") 133 | return 134 | }) 135 | 136 | // Update "Pairing" Invite on the Monitor 137 | updatePairingInvite(newVolume) 138 | } 139 | 140 | function updatePairingInvite(volume) { 141 | 142 | const MIN_LEVEL_TO_PAIR = 5 // note that this is an arbitrary value 143 | if (volume < MIN_LEVEL_TO_PAIR) { 144 | xapi.config.set('UserInterface CustomMessage', "/!\\ Pairing is disabled") 145 | return 146 | } 147 | 148 | // Pick the message that suits your device's registration mode 149 | xapi.status.get('Webex Status') 150 | .then((status) => { 151 | // If cloud-registered 152 | if (status == 'Registered') { 153 | xapi.config.set('UserInterface CustomMessage', "Tip: Launch Webex Teams to pair") 154 | } 155 | else { 156 | // If registered on-premises (VCS or CUCM ) 157 | xapi.config.set('UserInterface CustomMessage', "Tip: Pair from a Proximity client") 158 | } 159 | }) 160 | } 161 | -------------------------------------------------------------------------------- /controls/ultrasound/ultrasound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/controls/ultrasound/ultrasound.png -------------------------------------------------------------------------------- /controls/ultrasound/ultrasound.xml: -------------------------------------------------------------------------------- 1 | 2 | 1.6 3 | 4 | ULTRASOUND 5 | Home 6 | Proximity 7 | 1 8 | #FFB400 9 | Ultrasound 10 | Custom 11 | 12 | Ultrasound 13 | 14 | Volume 15 | 16 | volume_text 17 | ? 18 | Text 19 | size=1;fontSize=normal;align=center 20 | 21 | 22 | volume_slider 23 | Slider 24 | size=3 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /docs/QuickStart.md: -------------------------------------------------------------------------------- 1 | # QuickStart: Loading UI Extensions and Macros to a Collaboration Device 2 | 3 | Follow the steps below to load Controls and Macros. 4 | 5 | If you're looking for more details, check the [CE Customization Guide](https://www.cisco.com/c/dam/en/us/td/docs/telepresence/endpoint/ce99/sx-mx-dx-room-kit-customization-guide-ce99.pdf). 6 | 7 | 8 | ## Loading an In-Room Control 9 | 10 | Connect to your Device's local Web interface by entering its ip address in a Web Browser, and authenticate with your admin credentials. Example: http://192.168.1.30 11 | 12 | Launch the UI Extensions editor from the 'Integration > UI Extensions' Menu, and clicking the "Launch Editor" button. This would typically take you to http://192.168.1.30/web/roomcontrol/editor from the example above. 13 | 14 | Open the top right menu, click "Import from file". 15 | 16 | ![](./img/controls-import-from-file.png) 17 | 18 | Supposing you've clone this repository, select the source XML file [agenda-CLEUR.xml](../controls/agenda/agenda-CLEUR.xml) placed in the 'controls/agenda' subfolder. 19 | 20 | Now click the "Export configuration" button as show below to push the control to your device. 21 | 22 | ![](./img/controls-push-to-device.png) 23 | 24 | The upload is instantaneous. 25 | Simply reach to your Device and start using the control. 26 | 27 | In the next step, you'll get details on how to push a JavaScript macro on the Device so that custom logic is executed as end-users interact with the controls. 28 | 29 | 30 | ## Loading a JavaScript Macro 31 | 32 | First, repeat the steps above with the [levels.xml](../controls/levels/levels.xml) file placed in the 'controls/levels' subfolder, and push the control to the device. 33 | You can now interact with the control, but nothing really happens as no logic has been deployed to the device yet. 34 | 35 | Now, launch the Macro editor from the 'Integration > Macro Editor' Menu. This would typically take you to http://192.168.1.30/web/customization/macro from the example above. 36 | 37 | Click "Import from file..." and select the source JS file [levels.js](../controls/levels/levels.js) placed in the same 'controls/levels' subfolder as shown in Step 1 below. 38 | 39 | ![](./img/macro-import-from-file.png) 40 | 41 | Click the 'Save' icon as shown in Step 2 above. 42 | 43 | > If you get an error at this point, simply refresh the Web page. Example: hit http://192.168.1.30/web/customization/macro again. The error is due to a User Interface timeout as the macro was pushed to the device. But no worries, the macro did got pushed. 44 | 45 | The macro is now deployed on the Device. 46 | To activate it, click the toggle, and check the logs show 'Macro ready' 47 | 48 | ![](./img/macro-activate.png) 49 | 50 | > If you see any error in the logs, there's good chances you missed to deploy the Control associated to the macro. 51 | 52 | Done. Enjoy the game, and start writing your own controls! 53 | -------------------------------------------------------------------------------- /docs/img/controls-import-from-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/docs/img/controls-import-from-file.png -------------------------------------------------------------------------------- /docs/img/controls-push-to-device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/docs/img/controls-push-to-device.png -------------------------------------------------------------------------------- /docs/img/macro-activate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/docs/img/macro-activate.png -------------------------------------------------------------------------------- /docs/img/macro-import-from-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/docs/img/macro-import-from-file.png -------------------------------------------------------------------------------- /docs/img/playground/1-playground-first-launch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/docs/img/playground/1-playground-first-launch.png -------------------------------------------------------------------------------- /docs/img/playground/2-playground-import-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/docs/img/playground/2-playground-import-file.png -------------------------------------------------------------------------------- /docs/img/playground/3-playground-after-import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/docs/img/playground/3-playground-after-import.png -------------------------------------------------------------------------------- /docs/img/playground/4-playground-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/docs/img/playground/4-playground-preview.png -------------------------------------------------------------------------------- /jsxapi/.gitignore: -------------------------------------------------------------------------------- 1 | work/ 2 | 3 | node_modules/ 4 | 5 | package-lock.json -------------------------------------------------------------------------------- /jsxapi/0-quickstart.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * Simple script that connects to a device and shows the current volume level 8 | */ 9 | 10 | // Connect to the device 11 | const jsxapi = require('jsxapi'); 12 | const xapi = jsxapi.connect("ssh://192.168.1.34", { 13 | username: 'integrator', 14 | password: '' 15 | }); 16 | xapi.on('error', (err) => { 17 | console.error(`connexion failed: ${err}, exiting`); 18 | process.exit(1); 19 | }); 20 | 21 | xapi.on('ready', () => { 22 | console.log("connexion successful"); 23 | 24 | // Display current audio volume 25 | xapi.status 26 | .get('Audio Volume') 27 | .then((level) => { 28 | console.log(`Current volume level: ${level}`); 29 | 30 | // Ending script 31 | xapi.close(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /jsxapi/1-connect.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * Script that connects to a device, and checks for errors at connection 8 | * then displays current volume level, and attempts to change level if authorized 9 | * and finally exists gracefully. 10 | * 11 | * /!\ Changing the volume level requires admin priviledges (not authorized from the integrator role) 12 | */ 13 | 14 | // Connect to the device 15 | const jsxapi = require('jsxapi'); 16 | const xapi = jsxapi.connect("ssh://192.168.1.34", { 17 | username: 'admin', 18 | password: '' 19 | }); 20 | 21 | xapi.on('error', (err) => { 22 | switch (err) { 23 | case "client-socket": 24 | console.error("Could not connect: invalid URL."); 25 | process.exit(1); 26 | 27 | case "client-authentication": 28 | console.error("Could not connect: invalid credentials."); 29 | process.exit(1); 30 | 31 | case "client-timeout": 32 | console.error("Could not connect: timeout."); 33 | process.exit(1); 34 | 35 | default: 36 | console.error(`Encountered error: ${err}.`); 37 | process.exit(1); 38 | } 39 | }); 40 | 41 | 42 | xapi.on('ready', () => { 43 | console.log("connexion successful"); 44 | 45 | // Display current audio volume 46 | xapi.status 47 | .get('Audio Volume') 48 | .then((level) => { 49 | console.log(`Current volume level: ${level}`); 50 | }); 51 | 52 | // Reset volume to 50 53 | xapi.command('Audio Volume Set', { Level: "50" }) 54 | .then((result) => { 55 | console.log(`Reset volume: ${result.status}`); 56 | 57 | // Ending script 58 | xapi.close(); 59 | }) 60 | .catch((err) => { 61 | console.error(`Cannot reset volume, reason: ${err.message}`) 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /jsxapi/10-autoanswer.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * Auto-answer call example, filtering out not registered origins 8 | * 9 | * Integrator role: not supported, as tested as of June 2018 10 | * - OK: registering a feedback for /Call 11 | * - KO: accepting an incoming call 12 | * * 13 | */ 14 | 15 | // 16 | // Connect to the device 17 | // 18 | 19 | const jsxapi = require('jsxapi'); 20 | 21 | // Check args 22 | if (!process.env.JSXAPI_DEVICE_URL || !process.env.JSXAPI_USERNAME) { 23 | console.log("Please specify info to connect to your device as JSXAPI_DEVICE_URL, JSXAPI_USERNAME, JSXAPI_PASSWORD env variables"); 24 | console.log("Bash example: JSXAPI_DEVICE_URL='ssh://192.168.1.32' JSXAPI_USERNAME='integrator' JSXAPI_PASSWORD='integrator' node example.js"); 25 | process.exit(1); 26 | } 27 | 28 | // Empty passwords are supported 29 | const password = process.env.JSXAPI_PASSWORD ? process.env.JSXAPI_PASSWORD : ""; 30 | 31 | // Connect to the device 32 | console.log("connecting to your device..."); 33 | const xapi = jsxapi.connect(process.env.JSXAPI_DEVICE_URL, { 34 | username: process.env.JSXAPI_USERNAME, 35 | password: password 36 | }); 37 | xapi.on('error', (err) => { 38 | console.error(`connexion failed: ${err}, exiting`); 39 | process.exit(1); 40 | }); 41 | 42 | 43 | // 44 | // Code logic 45 | // 46 | 47 | xapi.on('ready', () => { 48 | console.log("connexion successful"); 49 | 50 | // Listen to call events 51 | xapi.status 52 | .on('Call', (call) => { 53 | 54 | switch (call.Status) { 55 | case "Ringing": 56 | console.log(`NEW call: ${call.id}`); 57 | 58 | // Filter depending the origin of the call 59 | if ((call.Direction == "Incoming") 60 | && (call.Protocol == "Spark") 61 | && (call.DisplayName == "Salon")) { 62 | 63 | // Accept incoming call 64 | console.log(`Accepting incoming call: ${call.id}`); 65 | xapi.command('Call Accept', { 66 | CallId: call.id 67 | }); 68 | } 69 | return; 70 | 71 | case "Connected": 72 | console.log(`Connected call: ${call.id}`); 73 | return; 74 | 75 | case "Disconnecting": 76 | console.log(`Disconnecting call: ${call.id}`); 77 | return; 78 | 79 | case "Idle": 80 | console.log(`Idle call: ${call.id}`); 81 | return; 82 | 83 | default: 84 | //console.log("DEBUG: ignoring event"); 85 | return; 86 | } 87 | }) 88 | }); 89 | -------------------------------------------------------------------------------- /jsxapi/11-update-wallpaper.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * Make Custom Wallpaper do update by rebooting the device as new background images are pushed 8 | * 9 | */ 10 | 11 | // 12 | // Connect to the device 13 | // 14 | 15 | const jsxapi = require('jsxapi'); 16 | 17 | // Check args 18 | if (!process.env.JSXAPI_DEVICE_URL || !process.env.JSXAPI_USERNAME) { 19 | console.log("Please specify info to connect to your device as JSXAPI_DEVICE_URL, JSXAPI_USERNAME, JSXAPI_PASSWORD env variables"); 20 | console.log("Bash example: JSXAPI_DEVICE_URL='ssh://192.168.1.32' JSXAPI_USERNAME='integrator' JSXAPI_PASSWORD='integrator' node example.js"); 21 | process.exit(1); 22 | } 23 | 24 | // Empty passwords are supported 25 | const password = process.env.JSXAPI_PASSWORD ? process.env.JSXAPI_PASSWORD : ""; 26 | 27 | // Connect to the device 28 | console.log("connecting to your device..."); 29 | const xapi = jsxapi.connect(process.env.JSXAPI_DEVICE_URL, { 30 | username: process.env.JSXAPI_USERNAME, 31 | password: password 32 | }); 33 | xapi.on('error', (err) => { 34 | console.error(`connexion failed: ${err}, exiting`); 35 | process.exit(1); 36 | }); 37 | 38 | 39 | // 40 | // Code logic 41 | // 42 | 43 | xapi.on('ready', () => { 44 | console.log("connexion successful"); 45 | 46 | xapi.event.on('UserInterface Branding Updated', function (event) { 47 | // Was the background been updated ? 48 | if (event.Type == "Background") { 49 | console.log("New wallpaper pushed to system"); 50 | 51 | // Restart the device to update the wallpaper 52 | xapi.command('SystemUnit Boot', { 53 | 'Action' :'Restart' 54 | }) 55 | .then(() => { 56 | console.log("Restarting device to update Wallpaper"); 57 | }) 58 | .catch((err) => { 59 | console.log(`Could not restart device: ${err.message}`); 60 | }) 61 | } 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /jsxapi/2-environment.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * Reads connection info from environment variables 8 | */ 9 | 10 | // 11 | // Connect to the device 12 | // 13 | 14 | // Check args 15 | if (!process.env.JSXAPI_DEVICE_URL || !process.env.JSXAPI_USERNAME) { 16 | console.log("Please specify info to connect to your device as JSXAPI_URL, JSXAPI_USERNAME, JSXAPI_PASSWORD env variables"); 17 | console.log("Bash example: JSXAPI_DEVICE_URL='ssh://192.168.1.34' JSXAPI_USERNAME='integrator' JSXAPI_PASSWORD='integrator' node example.js"); 18 | process.exit(1); 19 | } 20 | // Empty passwords are supported 21 | const password = process.env.JSXAPI_PASSWORD ? process.env.JSXAPI_PASSWORD : ""; 22 | 23 | // Connect to the device 24 | const jsxapi = require('jsxapi'); 25 | const xapi = jsxapi.connect(process.env.JSXAPI_DEVICE_URL, { 26 | username: process.env.JSXAPI_USERNAME, 27 | password: password 28 | }); 29 | 30 | // Errors management 31 | xapi.on('error', (err) => { 32 | switch (err) { 33 | case "client-socket": 34 | console.error("Could not connect: invalid URL."); 35 | process.exit(1); 36 | 37 | case "client-authentication": 38 | console.error("Could not connect: invalid credentials."); 39 | process.exit(1); 40 | 41 | case "client-timeout": 42 | console.error("Could not connect: timeout."); 43 | process.exit(1); 44 | 45 | default: 46 | console.error(`Encountered error: ${err}.`); 47 | process.exit(1); 48 | } 49 | }); 50 | 51 | 52 | // 53 | // Code logic 54 | // 55 | 56 | xapi.on('ready', () => { 57 | console.log("connexion successful"); 58 | 59 | // Display current audio volume 60 | xapi.status 61 | .get('Audio Volume') 62 | .then((level) => { 63 | console.log(`Current volume level: ${level}`); 64 | }) 65 | .then(() => { 66 | // Gracefully ends after delay 67 | const delay = process.env.DELAY || 5; // in seconds 68 | setTimeout(() => { 69 | // End 70 | console.log('Exiting.'); 71 | xapi.close(); 72 | }, delay * 1000); 73 | console.log(`Will exit gracefully in ${delay} seconds...`); 74 | }) 75 | }); 76 | -------------------------------------------------------------------------------- /jsxapi/3-dial.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * Dials a demo SIP address, and closes the call after 30 seconds 8 | */ 9 | 10 | 11 | // 12 | // Connect to the device 13 | // 14 | 15 | const jsxapi = require('jsxapi'); 16 | 17 | // Check args 18 | if (!process.env.JSXAPI_DEVICE_URL || !process.env.JSXAPI_USERNAME) { 19 | console.log("Please specify info to connect to your device as JSXAPI_URL, JSXAPI_USERNAME, JSXAPI_PASSWORD env variables"); 20 | console.log("Bash example: JSXAPI_DEVICE_URL='ssh://192.168.1.34' JSXAPI_USERNAME='integrator' JSXAPI_PASSWORD='integrator' node example.js"); 21 | process.exit(1); 22 | } 23 | 24 | // Empty passwords are supported 25 | const password = process.env.JSXAPI_PASSWORD ? process.env.JSXAPI_PASSWORD : ""; 26 | 27 | // Connect to the device 28 | console.log("connecting to your device..."); 29 | const xapi = jsxapi.connect(process.env.JSXAPI_DEVICE_URL, { 30 | username: process.env.JSXAPI_USERNAME, 31 | password: password 32 | }); 33 | xapi.on('error', (err) => { 34 | console.error(`connexion failed: ${err}, exiting`); 35 | process.exit(1); 36 | }); 37 | 38 | 39 | // 40 | // Code logic 41 | // 42 | 43 | xapi.on('ready', () => { 44 | console.log("connexion successful"); 45 | 46 | // Start a call 47 | xapi.command('Dial', { Number: 'fireplace@ivr.vc' }) 48 | .then((call) => { 49 | console.log(`Started call with status: ${call.status}, id: ${call.CallId}`); 50 | 51 | // Stop call after delay 52 | const delay = 20; 53 | setTimeout(() => { 54 | console.log('Disconnecting call, and exiting.'); 55 | 56 | xapi.command('Call Disconnect', { CallId: call.CallId }) 57 | .then(process.exit); 58 | }, delay * 1000); 59 | console.log(`Call with be disconnected in ${delay} seconds...`); 60 | 61 | }) 62 | .catch((err) => { 63 | // Frequent error here is to have several on-going calls 64 | // reason: "Maximum limit of active calls reached" 65 | console.error(`Error in call: ${err.message}`) 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /jsxapi/4-custom-messages.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * Modify some of your device's configuration settings 8 | * In this example, we'll change the Halfwake and Awake messages 9 | */ 10 | 11 | // 12 | // Connect to the device 13 | // 14 | 15 | const jsxapi = require('jsxapi'); 16 | 17 | // Check args 18 | if (!process.env.JSXAPI_DEVICE_URL || !process.env.JSXAPI_USERNAME) { 19 | console.info("Please specify info to connect to your device as JSXAPI_DEVICE_URL, JSXAPI_USERNAME, JSXAPI_PASSWORD env variables"); 20 | console.info("Bash example: JSXAPI_DEVICE_URL='ssh://192.168.1.34' JSXAPI_USERNAME='integrator' JSXAPI_PASSWORD='integrator' node example.js"); 21 | process.exit(1); 22 | } 23 | 24 | // Empty passwords are supported 25 | const password = process.env.JSXAPI_PASSWORD ? process.env.JSXAPI_PASSWORD : ""; 26 | 27 | // Connect to the device 28 | console.log("connecting to your device..."); 29 | const xapi = jsxapi.connect(process.env.JSXAPI_DEVICE_URL, { 30 | username: process.env.JSXAPI_USERNAME, 31 | password: password 32 | }); 33 | xapi.on('error', (err) => { 34 | console.error(`connexion failed: ${err}, exiting`); 35 | process.exit(1); 36 | }); 37 | 38 | 39 | // 40 | // Custom logic 41 | // 42 | 43 | xapi.on('ready', () => { 44 | console.log("connexion successful"); 45 | 46 | // Update Halfwake message 47 | xapi.config.set('UserInterface OSD HalfwakeMessage', "I am API addict") 48 | .then(() => { 49 | console.info('updated HalfwakeMessage') 50 | }) 51 | .catch((err) => { 52 | console.error(`could not update Halfwake message : ${err.message}`) 53 | }); 54 | 55 | // Update Awake message 56 | xapi.config.set('UserInterface CustomMessage', "I am G33K") 57 | .then(() => { 58 | console.log('updated Awake message') 59 | 60 | // Ending script 61 | xapi.close(); 62 | }) 63 | .catch((err) => { 64 | console.error(`could not update Awake message : ${err.message}`) 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /jsxapi/5-rolling-messages.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * Modify some of your device's configuration settings 8 | * In this example, we'll change the Halfwake and Awake messages 9 | */ 10 | 11 | // 12 | // Connect to the device 13 | // 14 | 15 | const jsxapi = require('jsxapi'); 16 | 17 | // Check args 18 | if (!process.env.JSXAPI_DEVICE_URL || !process.env.JSXAPI_USERNAME) { 19 | console.log("Please specify info to connect to your device as JSXAPI_DEVICE_URL, JSXAPI_USERNAME, JSXAPI_PASSWORD env variables"); 20 | console.log("Bash example: JSXAPI_DEVICE_URL='ssh://192.168.1.34' JSXAPI_USERNAME='integrator' JSXAPI_PASSWORD='integrator' node example.js"); 21 | process.exit(1); 22 | } 23 | 24 | // Empty passwords are supported 25 | const password = process.env.JSXAPI_PASSWORD ? process.env.JSXAPI_PASSWORD : ""; 26 | 27 | // Connect to a device 28 | const xapi = jsxapi.connect(process.env.JSXAPI_DEVICE_URL, { 29 | username: process.env.JSXAPI_USERNAME, 30 | password: password 31 | }); 32 | xapi.on('error', (err) => { 33 | console.error(`connexion failed: ${err}, exiting`); 34 | process.exit(1); 35 | }); 36 | 37 | 38 | // 39 | // Custom logic 40 | // 41 | 42 | function update(value) { 43 | 44 | // Update Awake message 45 | xapi.config.set('UserInterface CustomMessage', value) 46 | .then(() => { 47 | console.log('updated Awake message') 48 | }) 49 | .catch((err) => { 50 | console.error(`could not update Awake message : ${err.message}`) 51 | }); 52 | } 53 | 54 | // Iterator that rolls values among an array for a max number of times 55 | let current = {} 56 | current.options = []; 57 | current.iterations = 0; 58 | current.iterate = function (original, max) { 59 | // Reset options if array is empty 60 | if (this.options.length === 0) { 61 | this.options = original; 62 | } 63 | 64 | this.iterations++; 65 | if (this.iterations > max) { 66 | clearInterval(current.timer); 67 | return true; 68 | } 69 | 70 | // Pop next option 71 | var choice = this.options.pop(); 72 | update(choice); 73 | return false; 74 | } 75 | 76 | xapi.on('ready', () => { 77 | console.log("connexion successful"); 78 | 79 | const delay = 3; //seconds 80 | const iterations = 3; 81 | console.log(`Starting loop to roll Awake messages every ${delay} seconds, with ${iterations} iterations`); 82 | current.timer = setInterval(function () { 83 | const completed = current.iterate(["Read a tutorial", "Launch a Sandbox", "Attend an event", "Pick a DevNet activity among:"], iterations); 84 | 85 | // Roll completed 86 | if (completed) { 87 | console.log("Exiting."); 88 | xapi.close(); 89 | } 90 | }, delay * 1000); 91 | }); 92 | -------------------------------------------------------------------------------- /jsxapi/6-branding-logo.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * In this example, we'll update the Branding logo in Halfwake mode 8 | */ 9 | 10 | // 11 | // Connect to the device 12 | // 13 | 14 | const jsxapi = require('jsxapi'); 15 | 16 | // Check args 17 | if (!process.env.JSXAPI_DEVICE_URL || !process.env.JSXAPI_USERNAME) { 18 | console.log("Please specify info to connect to your device as JSXAPI_DEVICE_URL, JSXAPI_USERNAME, JSXAPI_PASSWORD env variables"); 19 | console.log("Bash example: JSXAPI_DEVICE_URL='ssh://192.168.1.34' JSXAPI_USERNAME='integrator' JSXAPI_PASSWORD='integrator' node example.js"); 20 | process.exit(1); 21 | } 22 | 23 | // Empty passwords are supported 24 | const password = process.env.JSXAPI_PASSWORD ? process.env.JSXAPI_PASSWORD : ""; 25 | 26 | // Connect to the device 27 | console.log("connecting to your device..."); 28 | const xapi = jsxapi.connect(process.env.JSXAPI_DEVICE_URL, { 29 | username: process.env.JSXAPI_USERNAME, 30 | password: password 31 | }); 32 | xapi.on('error', (err) => { 33 | console.error(`connexion failed: ${err}, exiting`); 34 | process.exit(1); 35 | }); 36 | xapi.on('ready', () => { 37 | console.log("connexion successful"); 38 | 39 | let encoded; 40 | try { 41 | // Read binary data 42 | const fs = require('fs'); 43 | const bitmap = fs.readFileSync("./img/create-logo-transparent.png"); 44 | 45 | // Convert binary data to base64 encoded string 46 | encoded = new Buffer(bitmap).toString('base64'); 47 | 48 | console.log("image encoding successful"); 49 | } 50 | catch (err) { 51 | console.error(`could not read image: ${err.message}, exiting`); 52 | process.exit(1); 53 | } 54 | 55 | // Update Awake message 56 | xapi.command('UserInterface Branding Upload', { 57 | Type: 'HalfwakeBranding', 58 | body: encoded 59 | }) 60 | .then(() => { 61 | console.log('updated Branding logo in Halfwake mode'); 62 | 63 | // Switch to Halwake mode 64 | xapi.command('Standby Halfwake') 65 | .then(() => xapi.close()); 66 | }) 67 | .catch((err) => { 68 | console.error(`could not update Brand logo: ${err.message}`); 69 | xapi.close(); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /jsxapi/7-feedback-peoplecount.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * Listen to realtime events via xAPI's feedback function 8 | * In this example, we display people count changes as they happen 9 | * 10 | * /!\ This example only works when run against a 'RoomKit' type of device 11 | */ 12 | 13 | // 14 | // Connect to the device 15 | // 16 | 17 | const jsxapi = require('jsxapi'); 18 | 19 | // Check args 20 | if (!process.env.JSXAPI_DEVICE_URL || !process.env.JSXAPI_USERNAME) { 21 | console.log("Please specify info to connect to your device as JSXAPI_DEVICE_URL, JSXAPI_USERNAME, JSXAPI_PASSWORD env variables"); 22 | console.log("Bash example: JSXAPI_DEVICE_URL='ssh://192.168.1.34' JSXAPI_USERNAME='integrator' JSXAPI_PASSWORD='integrator' node 7-feedback-peoplecount.js"); 23 | process.exit(1); 24 | } 25 | 26 | // Empty passwords are supported 27 | const password = process.env.JSXAPI_PASSWORD ? process.env.JSXAPI_PASSWORD : ""; 28 | 29 | // Connect to the device 30 | console.log("connecting to your device..."); 31 | const xapi = jsxapi.connect(process.env.JSXAPI_DEVICE_URL, { 32 | username: process.env.JSXAPI_USERNAME, 33 | password: password 34 | }); 35 | xapi.on('error', (err) => { 36 | console.error(`connexion failed: ${err}, exiting`); 37 | process.exit(1); 38 | }); 39 | 40 | 41 | // 42 | // Code logic 43 | // 44 | 45 | xapi.on('ready', () => { 46 | console.log("connexion successful"); 47 | 48 | // Fetch current count 49 | xapi.status 50 | .get('RoomAnalytics PeopleCount') 51 | .then((count) => { 52 | console.log(`Initial count is: ${count.Current}`); 53 | 54 | // Listen to events 55 | console.log('Adding feedback listener to: RoomAnalytics PeopleCount'); 56 | xapi.feedback.on('/Status/RoomAnalytics/PeopleCount', (count) => { 57 | console.log(`Updated count to: ${count.Current}`); 58 | }); 59 | 60 | }) 61 | .catch((err) => { 62 | console.log(`Failed to fetch PeopleCount, err: ${err.message}`); 63 | console.log(`Are you interacting with a RoomKit? exiting...`); 64 | xapi.close(); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /jsxapi/8-listen-audio-volume.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * Listens to "Status Audio Volume" changes as they happen on the device. 8 | */ 9 | 10 | 11 | // 12 | // Connect to the device 13 | // 14 | 15 | const jsxapi = require('jsxapi'); 16 | 17 | // Check args 18 | if (!process.env.JSXAPI_DEVICE_URL || !process.env.JSXAPI_USERNAME) { 19 | console.log("Please specify info to connect to your device as JSXAPI_URL, JSXAPI_USERNAME, JSXAPI_PASSWORD env variables"); 20 | console.log("Bash example: JSXAPI_DEVICE_URL='ssh://192.168.1.34' JSXAPI_USERNAME='integrator' JSXAPI_PASSWORD='integrator' node example.js"); 21 | process.exit(1); 22 | } 23 | 24 | // Empty passwords are supported 25 | const password = process.env.JSXAPI_PASSWORD ? process.env.JSXAPI_PASSWORD : ""; 26 | 27 | // Connect to the device 28 | console.log("connecting to your device..."); 29 | const xapi = jsxapi.connect(process.env.JSXAPI_DEVICE_URL, { 30 | username: process.env.JSXAPI_USERNAME, 31 | password: password 32 | }); 33 | xapi.on('error', (err) => { 34 | console.error(`connexion failed: ${err}, exiting`); 35 | process.exit(1); 36 | }); 37 | 38 | 39 | // 40 | // Code logic 41 | // 42 | 43 | xapi.on('ready', () => { 44 | console.log("connexion successful"); 45 | 46 | // Listen to events 47 | console.log('Please press the Audio (+) / (-) buttons'); 48 | const off = xapi.status.on('Audio Volume', (volume) => { 49 | console.log(`Volume changed to: ${volume}`) 50 | }); 51 | 52 | // Stop listening after delay 53 | const delay = 20; // in seconds 54 | setTimeout(() => { 55 | console.log('Exiting.'); 56 | 57 | // De-register feedback 58 | off(); 59 | xapi.close(); 60 | 61 | }, delay * 1000); 62 | console.log(`Will stop listening and exit in ${delay} seconds...`); 63 | 64 | }); 65 | -------------------------------------------------------------------------------- /jsxapi/9-listen-all-status.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | 7 | /** 8 | * Listens to all "Status" changes as they happen on the device. 9 | */ 10 | 11 | // 12 | // Connect to the device 13 | // 14 | 15 | const jsxapi = require('jsxapi'); 16 | 17 | // Check args 18 | if (!process.env.JSXAPI_DEVICE_URL || !process.env.JSXAPI_USERNAME) { 19 | console.log("Please specify info to connect to your device as JSXAPI_URL, JSXAPI_USERNAME, JSXAPI_PASSWORD env variables"); 20 | console.log("Bash example: JSXAPI_DEVICE_URL='ssh://192.168.1.34' JSXAPI_USERNAME='integrator' JSXAPI_PASSWORD='integrator' node example.js"); 21 | process.exit(1); 22 | } 23 | 24 | // Empty passwords are supported 25 | const password = process.env.JSXAPI_PASSWORD ? process.env.JSXAPI_PASSWORD : ""; 26 | 27 | // Connect to the device 28 | console.log("connecting to your device..."); 29 | const xapi = jsxapi.connect(process.env.JSXAPI_DEVICE_URL, { 30 | username: process.env.JSXAPI_USERNAME, 31 | password: password 32 | }); 33 | xapi.on('error', (err) => { 34 | console.error(`connexion failed: ${err}, exiting`); 35 | process.exit(1); 36 | }); 37 | 38 | 39 | // 40 | // Code logic 41 | // 42 | 43 | xapi.on('ready', () => { 44 | console.log("connexion successful"); 45 | 46 | // Listen to events 47 | console.log('Please start interacting with your device'); 48 | const off = xapi.feedback.on('/Status', (data) => { 49 | const type = Object.getOwnPropertyNames(data)[0]; 50 | if (type) { 51 | console.log(`Status changed for: ${type}`) 52 | } 53 | }); 54 | 55 | // Stop listening after delay 56 | const delay = 20; // in seconds 57 | setTimeout(() => { 58 | console.log('Exiting.'); 59 | 60 | // De-register feedback 61 | off(); 62 | xapi.close(); 63 | 64 | }, delay * 1000); 65 | console.log(`Will stop listening and exit in ${delay} seconds...`); 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /jsxapi/README.md: -------------------------------------------------------------------------------- 1 | # Example scripts using the Node.js jsxapi for Cisco Collaboration Devices 2 | 3 | The [Node.js jsxapi](https://github.com/cisco-ce/jsxapi) lets you create applications that interact with Cisco Collaboration Devices (DX, SX, MX, RoomKit, any CE-powered in fact). 4 | 5 | You'll find here scripts to learn the jsxapi through baby steps. 6 | 7 | **New to Room Devices, Controls & Macros? check the [QuickStart Guide](../docs/QuickStart.md) to learn to connect to your Device's Web Interface, and load Controls & Macros to your device** 8 | 9 | ## Quickstart 10 | 11 | Open a terminal and run the commands below: 12 | 13 | ```shell 14 | git clone https://github.com/ObjectIsAdvantag/xapi-samples 15 | cd xapi-samples 16 | cd jsxapi 17 | npm install 18 | 19 | # Place your device ip-address and credentials 20 | JSXAPI_DEVICE_URL='ssh://192.168.1.34' JSXAPI_USERNAME='integrator' JSXAPI_PASSWORD='integrator' node 8-rolling-messages 21 | ``` 22 | -------------------------------------------------------------------------------- /jsxapi/docs/4.0.0/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | document 13 | document 14 | 52% 15 | 52% 16 | 17 | 18 | -------------------------------------------------------------------------------- /jsxapi/docs/4.0.0/coverage.json: -------------------------------------------------------------------------------- 1 | { 2 | "coverage": "52.89%", 3 | "expectCount": 121, 4 | "actualCount": 64, 5 | "files": { 6 | "src/backend/index.js": { 7 | "expectCount": 11, 8 | "actualCount": 9, 9 | "undocumentLines": [ 10 | 46, 11 | 48 12 | ] 13 | }, 14 | "src/xapi/exc.js": { 15 | "expectCount": 18, 16 | "actualCount": 0, 17 | "undocumentLines": [ 18 | 2, 19 | 3, 20 | 4, 21 | 6, 22 | 7, 23 | 27, 24 | 34, 25 | 8, 26 | 5, 27 | 42, 28 | 1, 29 | 11, 30 | 22, 31 | 43, 32 | 28, 33 | 35, 34 | 12, 35 | 37 36 | ] 37 | }, 38 | "src/xapi/components.js": { 39 | "expectCount": 7, 40 | "actualCount": 7, 41 | "undocumentLines": [] 42 | }, 43 | "src/xapi/feedback.js": { 44 | "expectCount": 13, 45 | "actualCount": 11, 46 | "undocumentLines": [ 47 | 12, 48 | 13 49 | ] 50 | }, 51 | "src/xapi/mixins.js": { 52 | "expectCount": 9, 53 | "actualCount": 9, 54 | "undocumentLines": [] 55 | }, 56 | "src/json-parser.js": { 57 | "expectCount": 8, 58 | "actualCount": 2, 59 | "undocumentLines": [ 60 | 15, 61 | 17, 62 | 27, 63 | 32, 64 | 22, 65 | 21 66 | ] 67 | }, 68 | "src/transport/stream.js": { 69 | "expectCount": 8, 70 | "actualCount": 4, 71 | "undocumentLines": [ 72 | 39, 73 | 17, 74 | 18, 75 | 19 76 | ] 77 | }, 78 | "src/backend/tsh.js": { 79 | "expectCount": 17, 80 | "actualCount": 4, 81 | "undocumentLines": [ 82 | 44, 83 | 73, 84 | 59, 85 | 38, 86 | 39, 87 | 92, 88 | 102, 89 | 40, 90 | 41, 91 | 81, 92 | 82, 93 | 42, 94 | 145 95 | ] 96 | }, 97 | "src/backend/ws.js": { 98 | "expectCount": 9, 99 | "actualCount": 4, 100 | "undocumentLines": [ 101 | 45, 102 | 66, 103 | 49, 104 | 57, 105 | 61 106 | ] 107 | }, 108 | "src/xapi/index.js": { 109 | "expectCount": 11, 110 | "actualCount": 11, 111 | "undocumentLines": [] 112 | }, 113 | "src/index.js": { 114 | "expectCount": 1, 115 | "actualCount": 1, 116 | "undocumentLines": [] 117 | }, 118 | "src/transport/ssh.js": { 119 | "expectCount": 1, 120 | "actualCount": 1, 121 | "undocumentLines": [] 122 | }, 123 | "src/xapi/rpc.js": { 124 | "expectCount": 7, 125 | "actualCount": 0, 126 | "undocumentLines": [ 127 | 130, 128 | 70, 129 | 156, 130 | 40, 131 | 65, 132 | 165, 133 | 93 134 | ] 135 | }, 136 | "src/transport/tsh.js": { 137 | "expectCount": 1, 138 | "actualCount": 1, 139 | "undocumentLines": [] 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /jsxapi/docs/4.0.0/css/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: #718c00; } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: #8959a8; } 17 | 18 | /* a comment */ 19 | .com { 20 | color: #8e908c; } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: #4271ae; } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: #f5871f; } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #4d4d4c; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #4d4d4c; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #4d4d4c; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /jsxapi/docs/4.0.0/image/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | document 13 | document 14 | @ratio@ 15 | @ratio@ 16 | 17 | 18 | -------------------------------------------------------------------------------- /jsxapi/docs/4.0.0/image/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/jsxapi/docs/4.0.0/image/github.png -------------------------------------------------------------------------------- /jsxapi/docs/4.0.0/image/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/jsxapi/docs/4.0.0/image/search.png -------------------------------------------------------------------------------- /jsxapi/docs/4.0.0/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsxapi", 3 | "version": "4.0.1", 4 | "description": "JavaScript bindings for XAPI", 5 | "author": { 6 | "name": "Martin Øinæs Myrseth", 7 | "email": "mmyrseth@cisco.com" 8 | }, 9 | "license": "MIT", 10 | "engines": { 11 | "node": ">=6.x", 12 | "npm": ">=5.x" 13 | }, 14 | "main": "lib/index.js", 15 | "bin": { 16 | "jsxapi": "./lib/cli.js" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git@github.com:cisco-ce/jsxapi.git" 21 | }, 22 | "scripts": { 23 | "build": "npm run build:js && npm run build:docs", 24 | "build:docs": "esdoc -c esdoc.json", 25 | "build:js": "babel -d lib/ src/", 26 | "clean": "rimraf docs lib", 27 | "lint": "eslint .", 28 | "prepare": "npm run build", 29 | "prepublishOnly": "npm test", 30 | "start": "NODE_ENV=development node ./bin/cli.js", 31 | "test": "npm run lint && mocha", 32 | "tdd": "mocha --watch --reporter min" 33 | }, 34 | "dependencies": { 35 | "commander": "^2.13.0", 36 | "duplex-passthrough": "^1.0.2", 37 | "duplexer": "^0.1.1", 38 | "jsonparse": "^1.3.1", 39 | "loglevel": "^1.6.1", 40 | "ssh2": "^0.5.5", 41 | "url-parse": "^1.2.0", 42 | "ws": "^4.0.0", 43 | "xml-escape": "^1.1.0" 44 | }, 45 | "devDependencies": { 46 | "babel-cli": "^6.23.0", 47 | "babel-eslint": "^7.1.1", 48 | "babel-plugin-transform-builtin-extend": "^1.1.2", 49 | "babel-plugin-transform-class-properties": "^6.23.0", 50 | "babel-preset-es2015": "^6.22.0", 51 | "babel-register": "^6.23.0", 52 | "chai": "^3.5.0", 53 | "chai-as-promised": "^6.0.0", 54 | "chai-properties": "^1.2.3", 55 | "dirty-chai": "^1.2.2", 56 | "esdoc": "^0.4.8", 57 | "esdoc-es7-plugin": "0.0.3", 58 | "esdoc-importpath-plugin": "^0.1.1", 59 | "eslint": ">=4.18.2", 60 | "eslint-config-airbnb-base": "^11.1.0", 61 | "eslint-plugin-import": "^2.2.0", 62 | "json-loader": "^0.5.4", 63 | "mocha": "^3.2.0", 64 | "rimraf": "^2.6.1", 65 | "sinon": "^4.0.1", 66 | "sinon-chai": "^2.14.0" 67 | }, 68 | "babel": { 69 | "plugins": [ 70 | "transform-class-properties", 71 | [ 72 | "transform-builtin-extend", 73 | { 74 | "globals": [ 75 | "Error" 76 | ] 77 | } 78 | ] 79 | ], 80 | "presets": [ 81 | "es2015" 82 | ] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /jsxapi/docs/4.0.0/script/inherited-summary.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | function toggle(ev) { 3 | var button = ev.target; 4 | var parent = ev.target.parentElement; 5 | while(parent) { 6 | if (parent.tagName === 'TABLE' && parent.classList.contains('summary')) break; 7 | parent = parent.parentElement; 8 | } 9 | 10 | if (!parent) return; 11 | 12 | var tbody = parent.querySelector('tbody'); 13 | if (button.classList.contains('opened')) { 14 | button.classList.remove('opened'); 15 | button.classList.add('closed'); 16 | tbody.style.display = 'none'; 17 | } else { 18 | button.classList.remove('closed'); 19 | button.classList.add('opened'); 20 | tbody.style.display = 'block'; 21 | } 22 | } 23 | 24 | var buttons = document.querySelectorAll('.inherited-summary thead .toggle'); 25 | for (var i = 0; i < buttons.length; i++) { 26 | buttons[i].addEventListener('click', toggle); 27 | } 28 | })(); 29 | -------------------------------------------------------------------------------- /jsxapi/docs/4.0.0/script/inner-link.js: -------------------------------------------------------------------------------- 1 | // inner link(#foo) can not correctly scroll, because page has fixed header, 2 | // so, I manually scroll. 3 | (function(){ 4 | var matched = location.hash.match(/errorLines=([\d,]+)/); 5 | if (matched) return; 6 | 7 | function adjust() { 8 | window.scrollBy(0, -55); 9 | var el = document.querySelector('.inner-link-active'); 10 | if (el) el.classList.remove('inner-link-active'); 11 | 12 | // ``[ ] . ' " @`` are not valid in DOM id. so must escape these. 13 | var id = location.hash.replace(/([\[\].'"@$])/g, '\\$1'); 14 | var el = document.querySelector(id); 15 | if (el) el.classList.add('inner-link-active'); 16 | } 17 | 18 | window.addEventListener('hashchange', adjust); 19 | 20 | if (location.hash) { 21 | setTimeout(adjust, 0); 22 | } 23 | })(); 24 | 25 | (function(){ 26 | var els = document.querySelectorAll('[href^="#"]'); 27 | for (var i = 0; i < els.length; i++) { 28 | var el = els[i]; 29 | el.href = location.href + el.getAttribute('href'); // because el.href is absolute path 30 | } 31 | })(); 32 | -------------------------------------------------------------------------------- /jsxapi/docs/4.0.0/script/manual.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var matched = location.pathname.match(/([^/]*)\.html$/); 3 | if (!matched) return; 4 | 5 | var currentName = matched[1]; 6 | var cssClass = '.navigation [data-toc-name="' + currentName + '"]'; 7 | var styleText = cssClass + ' .manual-toc { display: block; }\n'; 8 | styleText += cssClass + ' .manual-toc-title { background-color: #039BE5; }\n'; 9 | styleText += cssClass + ' .manual-toc-title a { color: white; }\n'; 10 | var style = document.createElement('style'); 11 | style.textContent = styleText; 12 | document.querySelector('head').appendChild(style); 13 | })(); 14 | -------------------------------------------------------------------------------- /jsxapi/docs/4.0.0/script/patch-for-local.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | if (location.protocol === 'file:') { 3 | var elms = document.querySelectorAll('a[href="./"]'); 4 | for (var i = 0; i < elms.length; i++) { 5 | elms[i].href = './index.html'; 6 | } 7 | } 8 | })(); 9 | -------------------------------------------------------------------------------- /jsxapi/docs/4.0.0/script/pretty-print.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | prettyPrint(); 3 | var lines = document.querySelectorAll('.prettyprint.linenums li[class^="L"]'); 4 | for (var i = 0; i < lines.length; i++) { 5 | lines[i].id = 'lineNumber' + (i + 1); 6 | } 7 | 8 | var matched = location.hash.match(/errorLines=([\d,]+)/); 9 | if (matched) { 10 | var lines = matched[1].split(','); 11 | for (var i = 0; i < lines.length; i++) { 12 | var id = '#lineNumber' + lines[i]; 13 | var el = document.querySelector(id); 14 | el.classList.add('error-line'); 15 | } 16 | return; 17 | } 18 | 19 | if (location.hash) { 20 | // ``[ ] . ' " @`` are not valid in DOM id. so must escape these. 21 | var id = location.hash.replace(/([\[\].'"@$])/g, '\\$1'); 22 | var line = document.querySelector(id); 23 | if (line) line.classList.add('active'); 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /jsxapi/docs/4.0.0/script/search.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var searchIndex = window.esdocSearchIndex; 3 | var searchBox = document.querySelector('.search-box'); 4 | var input = document.querySelector('.search-input'); 5 | var result = document.querySelector('.search-result'); 6 | var selectedIndex = -1; 7 | var prevText; 8 | 9 | // active search box and focus when mouse enter on search box. 10 | searchBox.addEventListener('mouseenter', function(){ 11 | searchBox.classList.add('active'); 12 | input.focus(); 13 | }); 14 | 15 | // search with text when key is upped. 16 | input.addEventListener('keyup', function(ev){ 17 | var text = ev.target.value.toLowerCase(); 18 | if (!text) { 19 | result.style.display = 'none'; 20 | result.innerHTML = ''; 21 | return; 22 | } 23 | 24 | if (text === prevText) return; 25 | prevText = text; 26 | 27 | var html = {class: [], method: [], member: [], function: [], variable: [], typedef: [], external: [], file: [], test: [], testFile: []}; 28 | var len = searchIndex.length; 29 | var kind; 30 | for (var i = 0; i < len; i++) { 31 | var pair = searchIndex[i]; 32 | if (pair[0].indexOf(text) !== -1) { 33 | kind = pair[3]; 34 | html[kind].push('
  • ' + pair[2] + '
  • '); 35 | } 36 | } 37 | 38 | var innerHTML = ''; 39 | for (kind in html) { 40 | var list = html[kind]; 41 | if (!list.length) continue; 42 | innerHTML += '
  • ' + kind + '
  • \n' + list.join('\n'); 43 | } 44 | result.innerHTML = innerHTML; 45 | if (innerHTML) result.style.display = 'block'; 46 | selectedIndex = -1; 47 | }); 48 | 49 | // down, up and enter key are pressed, select search result. 50 | input.addEventListener('keydown', function(ev){ 51 | if (ev.keyCode === 40) { 52 | // arrow down 53 | var current = result.children[selectedIndex]; 54 | var selected = result.children[selectedIndex + 1]; 55 | if (selected && selected.classList.contains('search-separator')) { 56 | var selected = result.children[selectedIndex + 2]; 57 | selectedIndex++; 58 | } 59 | 60 | if (selected) { 61 | if (current) current.classList.remove('selected'); 62 | selectedIndex++; 63 | selected.classList.add('selected'); 64 | } 65 | } else if (ev.keyCode === 38) { 66 | // arrow up 67 | var current = result.children[selectedIndex]; 68 | var selected = result.children[selectedIndex - 1]; 69 | if (selected && selected.classList.contains('search-separator')) { 70 | var selected = result.children[selectedIndex - 2]; 71 | selectedIndex--; 72 | } 73 | 74 | if (selected) { 75 | if (current) current.classList.remove('selected'); 76 | selectedIndex--; 77 | selected.classList.add('selected'); 78 | } 79 | } else if (ev.keyCode === 13) { 80 | // enter 81 | var current = result.children[selectedIndex]; 82 | if (current) { 83 | var link = current.querySelector('a'); 84 | if (link) location.href = link.href; 85 | } 86 | } else { 87 | return; 88 | } 89 | 90 | ev.preventDefault(); 91 | }); 92 | 93 | // select search result when search result is mouse over. 94 | result.addEventListener('mousemove', function(ev){ 95 | var current = result.children[selectedIndex]; 96 | if (current) current.classList.remove('selected'); 97 | 98 | var li = ev.target; 99 | while (li) { 100 | if (li.nodeName === 'LI') break; 101 | li = li.parentElement; 102 | } 103 | 104 | if (li) { 105 | selectedIndex = Array.prototype.indexOf.call(result.children, li); 106 | li.classList.add('selected'); 107 | } 108 | }); 109 | 110 | // clear search result when body is clicked. 111 | document.body.addEventListener('click', function(ev){ 112 | selectedIndex = -1; 113 | result.style.display = 'none'; 114 | result.innerHTML = ''; 115 | }); 116 | 117 | })(); 118 | -------------------------------------------------------------------------------- /jsxapi/docs/4.0.0/script/test-summary.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | function toggle(ev) { 3 | var button = ev.target; 4 | var parent = ev.target.parentElement; 5 | while(parent) { 6 | if (parent.tagName === 'TR' && parent.classList.contains('test-describe')) break; 7 | parent = parent.parentElement; 8 | } 9 | 10 | if (!parent) return; 11 | 12 | var direction; 13 | if (button.classList.contains('opened')) { 14 | button.classList.remove('opened'); 15 | button.classList.add('closed'); 16 | direction = 'closed'; 17 | } else { 18 | button.classList.remove('closed'); 19 | button.classList.add('opened'); 20 | direction = 'opened'; 21 | } 22 | 23 | var targetDepth = parseInt(parent.dataset.testDepth, 10) + 1; 24 | var nextElement = parent.nextElementSibling; 25 | while (nextElement) { 26 | var depth = parseInt(nextElement.dataset.testDepth, 10); 27 | if (depth >= targetDepth) { 28 | if (direction === 'opened') { 29 | if (depth === targetDepth) nextElement.style.display = ''; 30 | } else if (direction === 'closed') { 31 | nextElement.style.display = 'none'; 32 | var innerButton = nextElement.querySelector('.toggle'); 33 | if (innerButton && innerButton.classList.contains('opened')) { 34 | innerButton.classList.remove('opened'); 35 | innerButton.classList.add('closed'); 36 | } 37 | } 38 | } else { 39 | break; 40 | } 41 | nextElement = nextElement.nextElementSibling; 42 | } 43 | } 44 | 45 | var buttons = document.querySelectorAll('.test-summary tr.test-describe .toggle'); 46 | for (var i = 0; i < buttons.length; i++) { 47 | buttons[i].addEventListener('click', toggle); 48 | } 49 | 50 | var topDescribes = document.querySelectorAll('.test-summary tr[data-test-depth="0"]'); 51 | for (var i = 0; i < topDescribes.length; i++) { 52 | topDescribes[i].style.display = ''; 53 | } 54 | })(); 55 | -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | document 13 | document 14 | 49% 15 | 49% 16 | 17 | 18 | -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/coverage.json: -------------------------------------------------------------------------------- 1 | { 2 | "coverage": "49%", 3 | "expectCount": 151, 4 | "actualCount": 74, 5 | "files": { 6 | "src/backend/index.js": { 7 | "expectCount": 11, 8 | "actualCount": 9, 9 | "undocumentLines": [ 10 | 46, 11 | 48 12 | ] 13 | }, 14 | "src/backend/tsh.js": { 15 | "expectCount": 23, 16 | "actualCount": 9, 17 | "undocumentLines": [ 18 | 26, 19 | 46, 20 | 47, 21 | 48, 22 | 49, 23 | 50, 24 | 52, 25 | 67, 26 | 81, 27 | 89, 28 | 90, 29 | 100, 30 | 110, 31 | 153 32 | ] 33 | }, 34 | "src/backend/ws.js": { 35 | "expectCount": 9, 36 | "actualCount": 4, 37 | "undocumentLines": [ 38 | 45, 39 | 49, 40 | 57, 41 | 61, 42 | 66 43 | ] 44 | }, 45 | "src/cli.js": { 46 | "expectCount": 2, 47 | "actualCount": 0, 48 | "undocumentLines": [ 49 | 13, 50 | 19 51 | ] 52 | }, 53 | "src/index.js": { 54 | "expectCount": 1, 55 | "actualCount": 1, 56 | "undocumentLines": [] 57 | }, 58 | "src/json-parser.js": { 59 | "expectCount": 10, 60 | "actualCount": 2, 61 | "undocumentLines": [ 62 | 15, 63 | 17, 64 | 21, 65 | 22, 66 | 27, 67 | 32, 68 | 38, 69 | 45 70 | ] 71 | }, 72 | "src/transport/ssh.js": { 73 | "expectCount": 1, 74 | "actualCount": 1, 75 | "undocumentLines": [] 76 | }, 77 | "src/transport/stream.js": { 78 | "expectCount": 10, 79 | "actualCount": 4, 80 | "undocumentLines": [ 81 | 17, 82 | 18, 83 | 19, 84 | 39, 85 | 46, 86 | 51 87 | ] 88 | }, 89 | "src/transport/tsh.js": { 90 | "expectCount": 2, 91 | "actualCount": 1, 92 | "undocumentLines": [ 93 | 6 94 | ] 95 | }, 96 | "src/xapi/components.js": { 97 | "expectCount": 11, 98 | "actualCount": 8, 99 | "undocumentLines": [ 100 | 51, 101 | 62, 102 | 74 103 | ] 104 | }, 105 | "src/xapi/exc.js": { 106 | "expectCount": 19, 107 | "actualCount": 0, 108 | "undocumentLines": [ 109 | 1, 110 | 2, 111 | 3, 112 | 4, 113 | 5, 114 | 6, 115 | 7, 116 | 8, 117 | 11, 118 | 12, 119 | 22, 120 | 24, 121 | 30, 122 | 31, 123 | 37, 124 | 38, 125 | 40, 126 | 45, 127 | 46 128 | ] 129 | }, 130 | "src/xapi/feedback.js": { 131 | "expectCount": 15, 132 | "actualCount": 11, 133 | "undocumentLines": [ 134 | 11, 135 | 12, 136 | 51, 137 | 55 138 | ] 139 | }, 140 | "src/xapi/index.js": { 141 | "expectCount": 14, 142 | "actualCount": 14, 143 | "undocumentLines": [] 144 | }, 145 | "src/xapi/mixins.js": { 146 | "expectCount": 9, 147 | "actualCount": 9, 148 | "undocumentLines": [] 149 | }, 150 | "src/xapi/normalizePath.js": { 151 | "expectCount": 1, 152 | "actualCount": 1, 153 | "undocumentLines": [] 154 | }, 155 | "src/xapi/rpc.js": { 156 | "expectCount": 13, 157 | "actualCount": 0, 158 | "undocumentLines": [ 159 | 11, 160 | 12, 161 | 15, 162 | 21, 163 | 40, 164 | 65, 165 | 70, 166 | 93, 167 | 98, 168 | 132, 169 | 139, 170 | 158, 171 | 167 172 | ] 173 | } 174 | } 175 | } -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/css/github.css: -------------------------------------------------------------------------------- 1 | /* github markdown */ 2 | .github-markdown { 3 | font-size: 16px; 4 | } 5 | 6 | .github-markdown h1, 7 | .github-markdown h2, 8 | .github-markdown h3, 9 | .github-markdown h4, 10 | .github-markdown h5 { 11 | margin-top: 1em; 12 | margin-bottom: 16px; 13 | font-weight: bold; 14 | padding: 0; 15 | } 16 | 17 | .github-markdown h1:nth-of-type(1) { 18 | margin-top: 0; 19 | } 20 | 21 | .github-markdown h1 { 22 | font-size: 2em; 23 | padding-bottom: 0.3em; 24 | } 25 | 26 | .github-markdown h2 { 27 | font-size: 1.75em; 28 | padding-bottom: 0.3em; 29 | } 30 | 31 | .github-markdown h3 { 32 | font-size: 1.5em; 33 | } 34 | 35 | .github-markdown h4 { 36 | font-size: 1.25em; 37 | } 38 | 39 | .github-markdown h5 { 40 | font-size: 1em; 41 | } 42 | 43 | .github-markdown ul, .github-markdown ol { 44 | padding-left: 2em; 45 | } 46 | 47 | .github-markdown pre > code { 48 | font-size: 0.85em; 49 | } 50 | 51 | .github-markdown table { 52 | margin-bottom: 1em; 53 | border-collapse: collapse; 54 | border-spacing: 0; 55 | } 56 | 57 | .github-markdown table tr { 58 | background-color: #fff; 59 | border-top: 1px solid #ccc; 60 | } 61 | 62 | .github-markdown table th, 63 | .github-markdown table td { 64 | padding: 6px 13px; 65 | border: 1px solid #ddd; 66 | } 67 | 68 | .github-markdown table tr:nth-child(2n) { 69 | background-color: #f8f8f8; 70 | } 71 | 72 | .github-markdown hr { 73 | border-right: 0; 74 | border-bottom: 1px solid #e5e5e5; 75 | border-left: 0; 76 | border-top: 0; 77 | } 78 | 79 | /** badge(.svg) does not have border */ 80 | .github-markdown img:not([src*=".svg"]) { 81 | max-width: 100%; 82 | box-shadow: 1px 1px 1px rgba(0,0,0,0.5); 83 | } 84 | -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/css/identifiers.css: -------------------------------------------------------------------------------- 1 | .identifiers-wrap { 2 | display: flex; 3 | align-items: flex-start; 4 | } 5 | 6 | .identifier-dir-tree { 7 | background: #fff; 8 | border: solid 1px #ddd; 9 | border-radius: 0.25em; 10 | top: 52px; 11 | position: -webkit-sticky; 12 | position: sticky; 13 | max-height: calc(100vh - 155px); 14 | overflow-y: scroll; 15 | min-width: 200px; 16 | margin-left: 1em; 17 | } 18 | 19 | .identifier-dir-tree-header { 20 | padding: 0.5em; 21 | background-color: #fafafa; 22 | border-bottom: solid 1px #ddd; 23 | } 24 | 25 | .identifier-dir-tree-content { 26 | padding: 0 0.5em 0; 27 | } 28 | 29 | .identifier-dir-tree-content > div { 30 | padding-top: 0.25em; 31 | padding-bottom: 0.25em; 32 | } 33 | 34 | .identifier-dir-tree-content a { 35 | color: inherit; 36 | } 37 | 38 | -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/css/manual.css: -------------------------------------------------------------------------------- 1 | .github-markdown .manual-toc { 2 | padding-left: 0; 3 | } 4 | 5 | .manual-index .manual-cards { 6 | display: flex; 7 | flex-wrap: wrap; 8 | } 9 | 10 | .manual-index .manual-card-wrap { 11 | width: 280px; 12 | padding: 10px 20px 10px 0; 13 | box-sizing: border-box; 14 | } 15 | 16 | .manual-index .manual-card-wrap > h1 { 17 | margin: 0; 18 | font-size: 1em; 19 | font-weight: 600; 20 | padding: 0.2em 0 0.2em 0.5em; 21 | border-radius: 0.1em 0.1em 0 0; 22 | border: none; 23 | } 24 | 25 | .manual-index .manual-card-wrap > h1 span { 26 | color: #555; 27 | } 28 | 29 | .manual-index .manual-card { 30 | height: 200px; 31 | overflow: hidden; 32 | border: solid 1px rgba(230, 230, 230, 0.84); 33 | border-radius: 0 0 0.1em 0.1em; 34 | padding: 8px; 35 | position: relative; 36 | } 37 | 38 | .manual-index .manual-card > div { 39 | transform: scale(0.4); 40 | transform-origin: 0 0; 41 | width: 250%; 42 | } 43 | 44 | .manual-index .manual-card > a { 45 | position: absolute; 46 | top: 0; 47 | left: 0; 48 | width: 100%; 49 | height: 100%; 50 | background: rgba(210, 210, 210, 0.1); 51 | } 52 | 53 | .manual-index .manual-card > a:hover { 54 | background: none; 55 | } 56 | 57 | .manual-index .manual-badge { 58 | margin: 0; 59 | } 60 | 61 | .manual-index .manual-user-index { 62 | margin-bottom: 1em; 63 | border-bottom: solid 1px #ddd; 64 | } 65 | 66 | .manual-root .navigation { 67 | padding-left: 4px; 68 | margin-top: 4px; 69 | } 70 | 71 | .navigation .manual-toc-root > div { 72 | padding-left: 0.25em; 73 | padding-right: 0.75em; 74 | } 75 | 76 | .github-markdown .manual-toc-title a { 77 | color: inherit; 78 | } 79 | 80 | .manual-breadcrumb-list { 81 | font-size: 0.8em; 82 | margin-bottom: 1em; 83 | } 84 | 85 | .manual-toc-title a:hover { 86 | color: #039BE5; 87 | } 88 | 89 | .manual-toc li { 90 | margin: 0.75em 0; 91 | list-style-type: none; 92 | } 93 | 94 | .navigation .manual-toc [class^="indent-h"] a { 95 | color: #666; 96 | } 97 | 98 | .navigation .manual-toc .indent-h1 a { 99 | color: #555; 100 | font-weight: 600; 101 | display: block; 102 | } 103 | 104 | .manual-toc .indent-h1 { 105 | display: block; 106 | margin: 0.4em 0 0 0.25em; 107 | padding: 0.2em 0 0.2em 0.5em; 108 | border-radius: 0.1em; 109 | } 110 | 111 | .manual-root .navigation .manual-toc li:not(.indent-h1) { 112 | margin-top: 0.5em; 113 | } 114 | 115 | .manual-toc .indent-h2 { 116 | display: none; 117 | margin-left: 1.5em; 118 | } 119 | .manual-toc .indent-h3 { 120 | display: none; 121 | margin-left: 2.5em; 122 | } 123 | .manual-toc .indent-h4 { 124 | display: none; 125 | margin-left: 3.5em; 126 | } 127 | .manual-toc .indent-h5 { 128 | display: none; 129 | margin-left: 4.5em; 130 | } 131 | 132 | .manual-nav li { 133 | margin: 0.75em 0; 134 | } 135 | -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/css/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: #718c00; } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: #8959a8; } 17 | 18 | /* a comment */ 19 | .com { 20 | color: #8e908c; } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: #4271ae; } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: #f5871f; } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #4d4d4c; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #4d4d4c; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #4d4d4c; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/css/search.css: -------------------------------------------------------------------------------- 1 | /* search box */ 2 | .search-box { 3 | position: absolute; 4 | top: 10px; 5 | right: 50px; 6 | padding-right: 8px; 7 | padding-bottom: 10px; 8 | line-height: normal; 9 | font-size: 12px; 10 | } 11 | 12 | .search-box img { 13 | width: 20px; 14 | vertical-align: top; 15 | } 16 | 17 | .search-input { 18 | display: inline; 19 | visibility: hidden; 20 | width: 0; 21 | padding: 2px; 22 | height: 1.5em; 23 | outline: none; 24 | background: transparent; 25 | border: 1px #0af; 26 | border-style: none none solid none; 27 | vertical-align: bottom; 28 | } 29 | 30 | .search-input-edge { 31 | display: none; 32 | width: 1px; 33 | height: 5px; 34 | background-color: #0af; 35 | vertical-align: bottom; 36 | } 37 | 38 | .search-result { 39 | position: absolute; 40 | display: none; 41 | height: 600px; 42 | width: 100%; 43 | padding: 0; 44 | margin-top: 5px; 45 | margin-left: 24px; 46 | background: white; 47 | box-shadow: 1px 1px 4px rgb(0,0,0); 48 | white-space: nowrap; 49 | overflow-y: scroll; 50 | } 51 | 52 | .search-result-import-path { 53 | color: #aaa; 54 | font-size: 12px; 55 | } 56 | 57 | .search-result li { 58 | list-style: none; 59 | padding: 2px 4px; 60 | } 61 | 62 | .search-result li a { 63 | display: block; 64 | } 65 | 66 | .search-result li.selected { 67 | background: #ddd; 68 | } 69 | 70 | .search-result li.search-separator { 71 | background: rgb(37, 138, 175); 72 | color: white; 73 | } 74 | 75 | .search-box.active .search-input { 76 | visibility: visible; 77 | transition: width 0.2s ease-out; 78 | width: 300px; 79 | } 80 | 81 | .search-box.active .search-input-edge { 82 | display: inline-block; 83 | } 84 | 85 | -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/css/source.css: -------------------------------------------------------------------------------- 1 | table.files-summary { 2 | width: 100%; 3 | margin: 10px 0; 4 | border-spacing: 0; 5 | border: 0; 6 | border-collapse: collapse; 7 | text-align: right; 8 | } 9 | 10 | table.files-summary tbody tr:hover { 11 | background: #eee; 12 | } 13 | 14 | table.files-summary td:first-child, 15 | table.files-summary td:nth-of-type(2) { 16 | text-align: left; 17 | } 18 | 19 | table.files-summary[data-use-coverage="false"] td.coverage { 20 | display: none; 21 | } 22 | 23 | table.files-summary thead { 24 | background: #fafafa; 25 | } 26 | 27 | table.files-summary td { 28 | border: solid 1px #ddd; 29 | padding: 4px 10px; 30 | vertical-align: top; 31 | } 32 | 33 | table.files-summary td.identifiers > span { 34 | display: block; 35 | margin-top: 4px; 36 | } 37 | table.files-summary td.identifiers > span:first-child { 38 | margin-top: 0; 39 | } 40 | 41 | table.files-summary .coverage-count { 42 | font-size: 12px; 43 | color: #aaa; 44 | display: inline-block; 45 | min-width: 40px; 46 | } 47 | 48 | .total-coverage-count { 49 | position: relative; 50 | bottom: 2px; 51 | font-size: 12px; 52 | color: #666; 53 | font-weight: 500; 54 | padding-left: 5px; 55 | } 56 | -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/css/test.css: -------------------------------------------------------------------------------- 1 | table.test-summary thead { 2 | background: #fafafa; 3 | } 4 | 5 | table.test-summary thead .test-description { 6 | width: 50%; 7 | } 8 | 9 | table.test-summary { 10 | width: 100%; 11 | margin: 10px 0; 12 | border-spacing: 0; 13 | border: 0; 14 | border-collapse: collapse; 15 | } 16 | 17 | table.test-summary thead .test-count { 18 | width: 3em; 19 | } 20 | 21 | table.test-summary tbody tr:hover { 22 | background-color: #eee; 23 | } 24 | 25 | table.test-summary td { 26 | border: solid 1px #ddd; 27 | padding: 4px 10px; 28 | vertical-align: top; 29 | } 30 | 31 | table.test-summary td p { 32 | margin: 0; 33 | } 34 | 35 | table.test-summary tr.test-interface .toggle { 36 | display: inline-block; 37 | float: left; 38 | margin-right: 4px; 39 | cursor: pointer; 40 | font-size: 0.8em; 41 | padding-top: 0.25em; 42 | } 43 | 44 | table.test-summary tr.test-interface .toggle.opened:before { 45 | content: '▼'; 46 | } 47 | 48 | table.test-summary tr.test-interface .toggle.closed:before { 49 | content: '▶'; 50 | } 51 | 52 | table.test-summary .test-target > span { 53 | display: block; 54 | margin-top: 4px; 55 | } 56 | table.test-summary .test-target > span:first-child { 57 | margin-top: 0; 58 | } 59 | -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/image/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | document 13 | document 14 | @ratio@ 15 | @ratio@ 16 | 17 | 18 | -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/image/esdoc-logo-mini-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/jsxapi/docs/4.2.0/image/esdoc-logo-mini-black.png -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/image/esdoc-logo-mini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/jsxapi/docs/4.2.0/image/esdoc-logo-mini.png -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/image/manual-badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | manual 13 | manual 14 | @value@ 15 | @value@ 16 | 17 | 18 | -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/image/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/jsxapi/docs/4.2.0/image/search.png -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/lint.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/script/inherited-summary.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | function toggle(ev) { 3 | var button = ev.target; 4 | var parent = ev.target.parentElement; 5 | while(parent) { 6 | if (parent.tagName === 'TABLE' && parent.classList.contains('summary')) break; 7 | parent = parent.parentElement; 8 | } 9 | 10 | if (!parent) return; 11 | 12 | var tbody = parent.querySelector('tbody'); 13 | if (button.classList.contains('opened')) { 14 | button.classList.remove('opened'); 15 | button.classList.add('closed'); 16 | tbody.style.display = 'none'; 17 | } else { 18 | button.classList.remove('closed'); 19 | button.classList.add('opened'); 20 | tbody.style.display = 'block'; 21 | } 22 | } 23 | 24 | var buttons = document.querySelectorAll('.inherited-summary thead .toggle'); 25 | for (var i = 0; i < buttons.length; i++) { 26 | buttons[i].addEventListener('click', toggle); 27 | } 28 | })(); 29 | -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/script/inner-link.js: -------------------------------------------------------------------------------- 1 | // inner link(#foo) can not correctly scroll, because page has fixed header, 2 | // so, I manually scroll. 3 | (function(){ 4 | var matched = location.hash.match(/errorLines=([\d,]+)/); 5 | if (matched) return; 6 | 7 | function adjust() { 8 | window.scrollBy(0, -55); 9 | var el = document.querySelector('.inner-link-active'); 10 | if (el) el.classList.remove('inner-link-active'); 11 | 12 | // ``[ ] . ' " @`` are not valid in DOM id. so must escape these. 13 | var id = location.hash.replace(/([\[\].'"@$])/g, '\\$1'); 14 | var el = document.querySelector(id); 15 | if (el) el.classList.add('inner-link-active'); 16 | } 17 | 18 | window.addEventListener('hashchange', adjust); 19 | 20 | if (location.hash) { 21 | setTimeout(adjust, 0); 22 | } 23 | })(); 24 | 25 | (function(){ 26 | var els = document.querySelectorAll('[href^="#"]'); 27 | var href = location.href.replace(/#.*$/, ''); // remove existed hash 28 | for (var i = 0; i < els.length; i++) { 29 | var el = els[i]; 30 | el.href = href + el.getAttribute('href'); // because el.href is absolute path 31 | } 32 | })(); 33 | -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/script/manual.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var matched = location.pathname.match(/\/(manual\/.*\.html)$/); 3 | if (!matched) return; 4 | 5 | var currentName = matched[1]; 6 | var cssClass = '.navigation .manual-toc li[data-link="' + currentName + '"]'; 7 | var styleText = cssClass + '{ display: block; }\n'; 8 | styleText += cssClass + '.indent-h1 a { color: #039BE5 }'; 9 | var style = document.createElement('style'); 10 | style.textContent = styleText; 11 | document.querySelector('head').appendChild(style); 12 | })(); 13 | -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/script/patch-for-local.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | if (location.protocol === 'file:') { 3 | var elms = document.querySelectorAll('a[href="./"]'); 4 | for (var i = 0; i < elms.length; i++) { 5 | elms[i].href = './index.html'; 6 | } 7 | } 8 | })(); 9 | -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/script/pretty-print.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | prettyPrint(); 3 | var lines = document.querySelectorAll('.prettyprint.linenums li[class^="L"]'); 4 | for (var i = 0; i < lines.length; i++) { 5 | lines[i].id = 'lineNumber' + (i + 1); 6 | } 7 | 8 | var matched = location.hash.match(/errorLines=([\d,]+)/); 9 | if (matched) { 10 | var lines = matched[1].split(','); 11 | for (var i = 0; i < lines.length; i++) { 12 | var id = '#lineNumber' + lines[i]; 13 | var el = document.querySelector(id); 14 | el.classList.add('error-line'); 15 | } 16 | return; 17 | } 18 | 19 | if (location.hash) { 20 | // ``[ ] . ' " @`` are not valid in DOM id. so must escape these. 21 | var id = location.hash.replace(/([\[\].'"@$])/g, '\\$1'); 22 | var line = document.querySelector(id); 23 | if (line) line.classList.add('active'); 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/script/search.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var searchIndex = window.esdocSearchIndex; 3 | var searchBox = document.querySelector('.search-box'); 4 | var input = document.querySelector('.search-input'); 5 | var result = document.querySelector('.search-result'); 6 | var selectedIndex = -1; 7 | var prevText; 8 | 9 | // active search box and focus when mouse enter on search box. 10 | searchBox.addEventListener('mouseenter', function(){ 11 | searchBox.classList.add('active'); 12 | input.focus(); 13 | }); 14 | 15 | // search with text when key is upped. 16 | input.addEventListener('keyup', function(ev){ 17 | var text = ev.target.value.toLowerCase(); 18 | if (!text) { 19 | result.style.display = 'none'; 20 | result.innerHTML = ''; 21 | return; 22 | } 23 | 24 | if (text === prevText) return; 25 | prevText = text; 26 | 27 | var html = {class: [], method: [], member: [], function: [], variable: [], typedef: [], external: [], file: [], test: [], testFile: []}; 28 | var len = searchIndex.length; 29 | var kind; 30 | for (var i = 0; i < len; i++) { 31 | var pair = searchIndex[i]; 32 | if (pair[0].indexOf(text) !== -1) { 33 | kind = pair[3]; 34 | html[kind].push('
  • ' + pair[2] + '
  • '); 35 | } 36 | } 37 | 38 | var innerHTML = ''; 39 | for (kind in html) { 40 | var list = html[kind]; 41 | if (!list.length) continue; 42 | innerHTML += '
  • ' + kind + '
  • \n' + list.join('\n'); 43 | } 44 | result.innerHTML = innerHTML; 45 | if (innerHTML) result.style.display = 'block'; 46 | selectedIndex = -1; 47 | }); 48 | 49 | // down, up and enter key are pressed, select search result. 50 | input.addEventListener('keydown', function(ev){ 51 | if (ev.keyCode === 40) { 52 | // arrow down 53 | var current = result.children[selectedIndex]; 54 | var selected = result.children[selectedIndex + 1]; 55 | if (selected && selected.classList.contains('search-separator')) { 56 | var selected = result.children[selectedIndex + 2]; 57 | selectedIndex++; 58 | } 59 | 60 | if (selected) { 61 | if (current) current.classList.remove('selected'); 62 | selectedIndex++; 63 | selected.classList.add('selected'); 64 | } 65 | } else if (ev.keyCode === 38) { 66 | // arrow up 67 | var current = result.children[selectedIndex]; 68 | var selected = result.children[selectedIndex - 1]; 69 | if (selected && selected.classList.contains('search-separator')) { 70 | var selected = result.children[selectedIndex - 2]; 71 | selectedIndex--; 72 | } 73 | 74 | if (selected) { 75 | if (current) current.classList.remove('selected'); 76 | selectedIndex--; 77 | selected.classList.add('selected'); 78 | } 79 | } else if (ev.keyCode === 13) { 80 | // enter 81 | var current = result.children[selectedIndex]; 82 | if (current) { 83 | var link = current.querySelector('a'); 84 | if (link) location.href = link.href; 85 | } 86 | } else { 87 | return; 88 | } 89 | 90 | ev.preventDefault(); 91 | }); 92 | 93 | // select search result when search result is mouse over. 94 | result.addEventListener('mousemove', function(ev){ 95 | var current = result.children[selectedIndex]; 96 | if (current) current.classList.remove('selected'); 97 | 98 | var li = ev.target; 99 | while (li) { 100 | if (li.nodeName === 'LI') break; 101 | li = li.parentElement; 102 | } 103 | 104 | if (li) { 105 | selectedIndex = Array.prototype.indexOf.call(result.children, li); 106 | li.classList.add('selected'); 107 | } 108 | }); 109 | 110 | // clear search result when body is clicked. 111 | document.body.addEventListener('click', function(ev){ 112 | selectedIndex = -1; 113 | result.style.display = 'none'; 114 | result.innerHTML = ''; 115 | }); 116 | 117 | })(); 118 | -------------------------------------------------------------------------------- /jsxapi/docs/4.2.0/script/test-summary.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | function toggle(ev) { 3 | var button = ev.target; 4 | var parent = ev.target.parentElement; 5 | while(parent) { 6 | if (parent.tagName === 'TR' && parent.classList.contains('test-interface')) break; 7 | parent = parent.parentElement; 8 | } 9 | 10 | if (!parent) return; 11 | 12 | var direction; 13 | if (button.classList.contains('opened')) { 14 | button.classList.remove('opened'); 15 | button.classList.add('closed'); 16 | direction = 'closed'; 17 | } else { 18 | button.classList.remove('closed'); 19 | button.classList.add('opened'); 20 | direction = 'opened'; 21 | } 22 | 23 | var targetDepth = parseInt(parent.dataset.testDepth, 10) + 1; 24 | var nextElement = parent.nextElementSibling; 25 | while (nextElement) { 26 | var depth = parseInt(nextElement.dataset.testDepth, 10); 27 | if (depth >= targetDepth) { 28 | if (direction === 'opened') { 29 | if (depth === targetDepth) nextElement.style.display = ''; 30 | } else if (direction === 'closed') { 31 | nextElement.style.display = 'none'; 32 | var innerButton = nextElement.querySelector('.toggle'); 33 | if (innerButton && innerButton.classList.contains('opened')) { 34 | innerButton.classList.remove('opened'); 35 | innerButton.classList.add('closed'); 36 | } 37 | } 38 | } else { 39 | break; 40 | } 41 | nextElement = nextElement.nextElementSibling; 42 | } 43 | } 44 | 45 | var buttons = document.querySelectorAll('.test-summary tr.test-interface .toggle'); 46 | for (var i = 0; i < buttons.length; i++) { 47 | buttons[i].addEventListener('click', toggle); 48 | } 49 | 50 | var topDescribes = document.querySelectorAll('.test-summary tr[data-test-depth="0"]'); 51 | for (var i = 0; i < topDescribes.length; i++) { 52 | topDescribes[i].style.display = ''; 53 | } 54 | })(); 55 | -------------------------------------------------------------------------------- /jsxapi/httpfeedback/README.md: -------------------------------------------------------------------------------- 1 | # Service to follow people count metrics in real time 2 | 3 | Start an HTTP Server that registers as a Webhook to your Room Device (via xAPI 's HttpFeedback) 4 | 5 | Open a terminal and run the commands as described below: 6 | 7 | ```shell 8 | git clone 9 | cd 10 | cd jsxapi 11 | cd httpfeedback 12 | npm install 13 | 14 | JSXAPI_DEVICE_URL='ssh://192.168.1.34' JSXAPI_USERNAME='admin' JSXAPI_PASSWORD='' WEBHOOK_URL="http://192.168.1.34:8080" node server.js 15 | ``` 16 | 17 | Check the service is running by hitting its healthcheck at http://localhost:8080 and http://192.168.1.34:8080 in the example above. 18 | -------------------------------------------------------------------------------- /jsxapi/httpfeedback/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "httpfeedback", 3 | "version": "0.1.0", 4 | "description": "HTTP Service to register for real-time HTTPFeedback events", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Stève Sfartz ", 10 | "license": "MIT", 11 | "dependencies": { 12 | "express": "^4.16.2", 13 | "jsxapi": "^4.0.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /jsxapi/httpfeedback/server.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * Registers an HTTP Webhook to receive People Count events from the Room Devices. 8 | * Note that HttpFeedback is the exact xAPI terminology. 9 | * 10 | * /!\ The user must have admin access to the device. Integrators cannot register httpfeedbacks 11 | * 12 | */ 13 | 14 | 15 | // 16 | // Connect to a RoomKit device 17 | // 18 | 19 | const jsxapi = require('jsxapi'); 20 | 21 | // Check args 22 | if (!process.env.JSXAPI_DEVICE_URL || !process.env.JSXAPI_USERNAME) { 23 | console.info("Please specify info to connect to your device as JSXAPI_URL, JSXAPI_USERNAME, JSXAPI_PASSWORD env variables"); 24 | console.info("Bash example: JSXAPI_DEVICE_URL='ssh://192.168.1.34' JSXAPI_USERNAME='integrator' JSXAPI_PASSWORD='integrator' node example.js"); 25 | process.exit(1); 26 | } 27 | if (!process.env.WEBHOOK_URL) { 28 | console.info("Please specify the URL to which events will be posted as a WEBHOOK_URL env variable"); 29 | process.exit(1); 30 | } 31 | // Empty passwords are supported 32 | const password = process.env.JSXAPI_PASSWORD ? process.env.JSXAPI_PASSWORD : ""; 33 | 34 | // Connect to the device 35 | const xapi = jsxapi.connect(process.env.JSXAPI_DEVICE_URL, { 36 | username: process.env.JSXAPI_USERNAME, 37 | password: password, 38 | }); 39 | xapi.on('error', (error) => { 40 | console.debug(`connexion failed: ${error.message}, exiting`); 41 | process.exit(1); 42 | }); 43 | xapi.on('ready', () => { 44 | console.debug("connexion successful"); 45 | 46 | // Registering webhook 47 | xapi.command('HttpFeedback Register', { 48 | FeedbackSlot: "2", 49 | ServerUrl: process.env.WEBHOOK_URL, 50 | Format: "JSON", 51 | Expression: "/Status/RoomAnalytics/PeopleCount" 52 | }) 53 | .then((result) => { 54 | console.log(`successfully registered Webhook for PeopleCount, result: ${result.status}, on slot: ${result.FeedbackSlot}`); 55 | }) 56 | .catch((error) => { 57 | console.log(`Could not register webhook against the device, reason: ${error.message}`); 58 | console.log(`Exiting...`); 59 | process.exit(1); 60 | }); 61 | }); 62 | 63 | 64 | // 65 | // Server receive the events 66 | // 67 | 68 | var express = require("express"); 69 | var app = express(); 70 | 71 | var bodyParser = require("body-parser"); 72 | app.use(bodyParser.urlencoded({ extended: true })); 73 | app.use(bodyParser.json()); 74 | 75 | var debug = require("debug")("samples"); 76 | 77 | 78 | var started = Date.now(); 79 | app.route("/") 80 | // healthcheck 81 | .get(function (req, res) { 82 | res.json({ 83 | message: "Congrats, your Room Analytics Service is up and running", 84 | since: new Date(started).toISOString() 85 | }); 86 | }) 87 | 88 | // webhook endpoint 89 | .post(function (req, res) { 90 | 91 | // analyse incoming payload, should conform to Spark webhook trigger specifications 92 | debug("DEBUG: webhook invoked"); 93 | if (!req.body) { 94 | console.log("WARNING: Unexpected payload received, aborting..."); 95 | res.status(400).json({ message: "Bad payload" }); 96 | return; 97 | } 98 | 99 | // event is ready to be processed, let's respond without waiting any longer 100 | res.status(200).json({ message: "message is being processed..." }); 101 | 102 | // process incoming resource/event, see https://developer.ciscospark.com/webhooks-explained.html 103 | processEvent(req.body); 104 | }); 105 | 106 | 107 | // Starts the Bot service 108 | // 109 | // [WORKAROUND] in some container situation (ie, Cisco Shipped), we need to use an OVERRIDE_PORT to force our bot to start and listen to the port defined in the Dockerfile (ie, EXPOSE), 110 | // and not the PORT dynamically assigned by the host or scheduler. 111 | var port = process.env.OVERRIDE_PORT || process.env.PORT || 8080; 112 | app.listen(port, function () { 113 | console.log("Cisco Spark Bot started at http://localhost:" + port + "/"); 114 | console.log(" GET / for health checks"); 115 | console.log(" POST / to send Webhook events"); 116 | }); 117 | 118 | 119 | // Invoked every time an event flies to the webhook 120 | function processEvent(payload) { 121 | //console.debug(`received new event`); 122 | 123 | const count =parseInt(payload.Status.RoomAnalytics.PeopleCount.Current.Value); 124 | console.log(`Count: ${count}`); 125 | } -------------------------------------------------------------------------------- /jsxapi/img/cisco-logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/jsxapi/img/cisco-logo-white.png -------------------------------------------------------------------------------- /jsxapi/img/create-logo-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/jsxapi/img/create-logo-transparent.png -------------------------------------------------------------------------------- /jsxapi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tutorial", 3 | "version": "0.2.1", 4 | "description": "Example Scripts using the Node.js jsxapi for Cisco Collaboration Devices", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "Stève Sfartz ", 9 | "license": "MIT", 10 | "dependencies": { 11 | "jsxapi": "^4.1.3" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /macros/1-helloworld.js: -------------------------------------------------------------------------------- 1 | console.log("Hello world") -------------------------------------------------------------------------------- /macros/10-httpclient.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Posts a message to a Webex Teams space via a Bot account 3 | * 4 | * Prep work: 5 | * from a ssh session, type the commands below: 6 | * > xConfiguration HttpClient Mode: On 7 | * > xConfiguration HttpClient AllowInsecureHTTPS: True 8 | * 9 | * Example for tshell: 10 | * > xcommand HttpClient Post 11 | * Url: https://api.ciscospark.com/v1/messages 12 | * Header: "Content-Type: application/json" 13 | * Header: "Authorization: Bearer BOT_TOKEN" 14 | * ResultBody: "plaintext" 15 | * AllowInsecureHTTPS: True 16 | * > {"markdown":"hey, this is Steve","roomId":"ROOM_ID"} 17 | * > . 18 | * 19 | * 20 | */ 21 | 22 | const xapi = require('xapi'); 23 | 24 | // Replace with your bot token 25 | const token = "BOT_TOKEN"; 26 | // replace with a space your bot is part of 27 | const roomId = "ROOM_ID"; 28 | 29 | function push(msg, cb) { 30 | 31 | // Post message 32 | let payload = { 33 | "markdown": msg, 34 | "roomId": roomId 35 | } 36 | xapi.command( 37 | 'HttpClient Post', 38 | { 39 | Header: ["Content-Type: application/json", "Authorization: Bearer " + token], 40 | Url: "https://api.ciscospark.com/v1/messages", 41 | AllowInsecureHTTPS: "True", 42 | ResultBody: 'plaintext' 43 | }, 44 | JSON.stringify(payload)) 45 | .then((response) => { 46 | console.debug(`received response with status code: ${response.StatusCode}`); 47 | 48 | if (response.StatusCode == 200) { 49 | console.log("message pushed to Webex Teams"); 50 | 51 | // Retrieve message id 52 | let result = JSON.parse(response.Body); 53 | console.log(`message id: ${result.id}`); 54 | if (cb) cb(null, result.id); 55 | return; 56 | } 57 | 58 | // This should not happen as Webex REST API always return 200 OK for POST requests 59 | console.log("failed with status code: " + response.StatusCode); 60 | if (cb) cb("failed with status code: " + response.StatusCode, response.StatusCode); 61 | }) 62 | .catch((err) => { 63 | console.log(`failed with err message: ${err.message}`); 64 | 65 | switch (err.message) { 66 | case 'Unknown command': 67 | // Can be caught at coding time 68 | console.log("seems a wrong HttpClient Verb is used"); 69 | break; 70 | 71 | case 'HttpClientPostResult': 72 | case 'HttpClientDeleteResult': 73 | console.log(`failed with err status: ${err.data.status}`); 74 | console.log(Object.keys(err.data)); 75 | if (err.data.status === 'Error') { 76 | 77 | // Typically: hostname not found 78 | if (err.data.Message) { 79 | console.log("data message: " + err.data.Message); 80 | break; 81 | } 82 | 83 | // Typically: the response status code is 4xx or 5xx 84 | if (err.data.StatusCode) { 85 | console.log("status code: " + err.data.StatusCode); 86 | 87 | // Note: err.data.Headers can also be retrieved, though not the body of the response (no ResponseBody attribute here) 88 | break; 89 | } 90 | } 91 | } 92 | 93 | if (cb) cb("Could not post message to Webex Teams", null); 94 | }) 95 | } 96 | 97 | push('Hey, this is Steve', console.log); 98 | 99 | /* 100 | xapi.event.on('UserInterface Extensions Event Clicked', (event) => { 101 | console.log(`new event from: ${event.Signal}`) 102 | 103 | // Push info about the session 104 | push('Hello World') 105 | }) 106 | */ -------------------------------------------------------------------------------- /macros/11-philips-hue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Post API requests to a Hue Bridge 3 | * 4 | */ 5 | 6 | const xapi = require("xapi") 7 | 8 | // Update for your Hue deployment 9 | const BRIDGE_IP = "192.168.1.33"; 10 | const BRIDGE_USER = "SECRET"; 11 | const LIGHT_ID = 1; // number of your Bulb as registered at your Hue Bridge 12 | 13 | // Change color 14 | const color = 25500; 15 | sendToBridge(BRIDGE_IP, BRIDGE_USER, LIGHT_ID, "Put", { "hue": color }); 16 | 17 | 18 | function sendToBridge(bridgeip, username, light, verb, payload) { 19 | 20 | xapi.command("HttpClient " + verb, { 21 | Header: ["Content-Type: application/json"], 22 | Url: `http://${bridgeip}/api/${username}/lights/${light}/state`, 23 | AllowInsecureHTTPS: "True", 24 | ResultBody: "plaintext" 25 | }, 26 | JSON.stringify(payload)) 27 | .then((response) => { 28 | 29 | console.log(`request sent to the bridge, status code: ${response.StatusCode}`); 30 | console.debug(`received response: ${response.Body}`); 31 | 32 | // Check response 33 | let result = JSON.parse(response.Body); 34 | if (result[0] && (result[0].success)) { 35 | console.log("success"); 36 | return; 37 | } 38 | 39 | if (result[0] && (result[0].error)) { 40 | console.log("error"); 41 | console.debug(result[0].error.Message); 42 | return; 43 | } 44 | 45 | console.warn(`unexpected response: ${Object.keys(result[0])}`); 46 | return; 47 | }) 48 | .catch((err) => { 49 | console.log("Could not contact the bridge"); 50 | console.debug("failed with err: " + err.message); 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /macros/12a-primary.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2019 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * Pushes an event to a remote Codec using the Message command and an HttpClient POST 8 | * This code needs a listener: install the secondary.js macro on the remote Codec 9 | * 10 | */ 11 | 12 | const xapi = require('xapi'); 13 | 14 | const SECONDARY_IP = "192.168.1.32"; 15 | const SECONDARY_BASICAUTH = "bG9jYWxhZG1pbjpjaXNjb3BzZHQ="; 16 | 17 | var EVENTURL = `http://${SECONDARY_IP}/putxml`; 18 | var HEADERS = ['Content-Type: text/xml', `Authorization: Basic ${SECONDARY_BASICAUTH}`]; 19 | 20 | function postRequest(url, payload, headers) { 21 | xapi.command('HttpClient Post', { 22 | Url: url, 23 | Header: headers 24 | }, payload).then((response) => { console.log(JSON.stringify(response)) }); 25 | console.log(payload); 26 | } 27 | 28 | function sendEvent(event) { 29 | event = event.replace(/"/g, "\'"); 30 | var payload = "" + event + ""; 31 | postRequest(EVENTURL, payload, HEADERS); 32 | } 33 | 34 | function volumeSync(volume) { 35 | sendEvent(JSON.stringify({ "Audio Volume Set": { "Level": volume } })); 36 | } 37 | 38 | xapi.status.on('Audio Volume', volumeSync); 39 | 40 | console.log('primary started') -------------------------------------------------------------------------------- /macros/12b-secondary.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2019 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * Listen to events sent from a remote Codec using the Message command and an HttpClient POST 8 | * This code needs a primary: install the primary.js macro on the remote Codec 9 | * 10 | */ 11 | const xapi = require('xapi'); 12 | 13 | function parse(event) { 14 | return JSON.parse(event); 15 | } 16 | 17 | xapi.event.on('Message Send Text', event => { 18 | event = event.replace(/'/g, '"'); 19 | try { 20 | var command = parse(event); 21 | for (var key in command) { 22 | console.debug(`processing command: ${key}`) 23 | xapi.command(key, command[key]); 24 | } 25 | } catch (err) { 26 | console.log(`cannot parse event: ${err.message}`); 27 | } 28 | }); 29 | 30 | console.log('secondary started'); -------------------------------------------------------------------------------- /macros/13a-environment.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * Implements Environment Variables for Macros: 8 | * A* environment.js : this macro manages volatile ENV variables (stores/updates and returns the current values for an environment variable) 9 | * B. getenv.js : this is not a macro but a code snippet that you insert into existing macros to retrieve ENV variables (local to your device) 10 | * 11 | * Quick start: 12 | * - customize the default list of ENV variables for your device 13 | * - deploy the 'environment.js' macro to your device 14 | * - insert the 'getenv()' code snipped into other macros on the same device 15 | * - access ENV variables from macros once 'env-ready' event is emmitted 16 | * 17 | * Note: if a variable is not found in ENV, '' is returned (empty) 18 | * 19 | * Example of a Macro using the getenv() function: 20 | * 21 | * // Fired when the environnment Macro is ready to provide ENV variables 22 | * xapi.on('env-ready', async (ready) => { 23 | * if (!ready) { 24 | * console.log('Environment macro is not responding! aborting...'); 25 | * return; 26 | * } 27 | * 28 | * let variable = 'DEVICE_SECRET' 29 | * let secret = await getenv(variable); 30 | * console.log(`echo \$${variable} = ${secret}`); 31 | * }); 32 | * 33 | */ 34 | 35 | const xapi = require('xapi'); 36 | 37 | // List of variables for the environment 38 | const ENV = { 39 | 'PING': 'PONG', 40 | 41 | // ADD environment variables below 42 | 'DEVICE_SECRET' : 1234 43 | }; 44 | 45 | xapi.event.on('Message Send Text', (msg) => { 46 | console.debug(`new "Message Send" Event with text: ${msg}`); 47 | 48 | // WORKAROUND: needed if message was sent from the cloud via POST https://api.ciscospark.com/v1/xapi/command/message.send 49 | msg = msg.replace(/\'/g, '"'); 50 | let parsed; 51 | try { 52 | parsed = JSON.parse(msg); 53 | } 54 | catch (err) { 55 | console.err(`cannot JSON parse the text in 'Message Send' Event: ${msg}, ignoring...`); 56 | return 57 | } 58 | 59 | let data = parsed; 60 | if (!data.env) { 61 | console.debug(`"Message Send" Event is not about Env: ${msg}, ignoring`); 62 | return; 63 | } 64 | 65 | console.debug(`"Message Sent" event concerns env variable: "${data.env}"`); 66 | 67 | // GET ENV 68 | if (data.operation && (data.operation == 'get')) { 69 | 70 | if (!data.env) { 71 | console.warn(`no ENV variable specified in: "${msg}", ignoring...`); 72 | return; 73 | } 74 | 75 | console.debug(`requested value for env variable: "${data.env}"`); 76 | let response = { 77 | 'env': data.env, 78 | 'operation': 'get_response', 79 | 'value': '' // default to '' if variable is not found 80 | } 81 | let value = ENV[data.env]; 82 | if (value) { 83 | response.value = value; 84 | } 85 | 86 | // Publish value for ENV variable 87 | xapi.command('Message Send', { Text: JSON.stringify(response) }); 88 | return; 89 | } 90 | 91 | // RESPONSE provided => ignore 92 | if (data.operation && (data.operation == 'get_response')) { 93 | console.debug(`ignoring get_response type of "Message Sent" event: ${msg}`); 94 | return; 95 | } 96 | 97 | // SET ENV 98 | // Note that the storage is volatile (dynamically added variables will be lost every time the macro or runtime is restared) 99 | if (data.operation && (data.operation == 'set')) { 100 | console.log(`setting value: "${data.value}" for env variable: "${data.env}"`); 101 | 102 | ENV[data.env] = data.value; 103 | return; 104 | } 105 | 106 | console.warn(`operation for "Message Sent" event is not supported:"${msg}, ignoring...`); 107 | return; 108 | }) 109 | -------------------------------------------------------------------------------- /macros/14-persist.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * JSON Database as a Macro: persists JSON to a macro with read and write functions 8 | * 9 | * examples: 10 | * write({'congrats':'you did it'}) 11 | * 12 | * read().then((data) => console.log(data.foo)) 13 | * 14 | * async () => { 15 | * await write({ player : 'steve', score: 5 }); 16 | * let data = await read(); 17 | * console.log(`player score is: ${data.score}`) 18 | * } 19 | * 20 | */ 21 | 22 | const xapi = require('xapi'); 23 | 24 | const PREFIX = 'const json = '; 25 | const DATABASE_NAME = 'DB'; // name of the macro with db contents 26 | 27 | // Read database contents 28 | async function read() { 29 | // Load contents 30 | let contents; 31 | try { 32 | let macro = await xapi.command('Macros Macro Get', { Name: DATABASE_NAME, Content: true }) 33 | contents = macro.Macro[0].Content.substring(PREFIX.length); 34 | } 35 | catch(err) { 36 | console.error(`cannot load contents from macro: ${DATABASE_NAME}`); 37 | throw new Error("DB_ACCESS_ERROR"); 38 | } 39 | 40 | // Parse contents 41 | try { 42 | console.debug(`DB contains: ${contents}`); 43 | let data = JSON.parse(contents); 44 | console.debug('DB successfully parsed'); 45 | return data; 46 | } 47 | catch (err) { 48 | console.error('DB is corrupted, cannot JSON parse the DB'); 49 | throw new Error('DB_PARSE_ERROR'); 50 | } 51 | } 52 | 53 | // Write database contents 54 | async function write(data) { 55 | // Serialize data as JSON and append prefix 56 | let contents; 57 | try { 58 | contents = PREFIX + JSON.stringify(data); 59 | } 60 | catch (err) { 61 | console.debug('Contents cannot be serialized to JSON'); 62 | throw new Error('DB_SERIALIZE_ERROR'); 63 | } 64 | 65 | // Write 66 | try { 67 | let res = await xapi.command('Macros Macro Save', { Name: DATABASE_NAME, OverWrite: true, body: contents }); 68 | return (res.status == 'OK'); 69 | } 70 | catch (err) { 71 | console.error(`cannot write contents to macro: ${DATABASE_NAME}`); 72 | throw new Error('DB_ACCESS_ERROR'); 73 | } 74 | } 75 | 76 | // read example 77 | /* 78 | read() 79 | .then((data) => console.log(data.foo)) 80 | .catch((err) => { 81 | switch (err.message) { 82 | case 'DB_ACCESS_ERROR': 83 | console.log("db not found: is the DB macro deployed ?"); 84 | break; 85 | case 'DB_PARSE_ERROR': 86 | console.log(`cannot parse: check the contents are JSON with prefix: ${PREFIX}, in macro with name: ${DATABASE_NAME}`); 87 | break; 88 | } 89 | }); 90 | */ 91 | 92 | // write example 93 | /* 94 | write({'congrats':'you did it'}) 95 | .then((success) => { 96 | if (success) { 97 | console.log('contents persisted'); 98 | } 99 | }) 100 | .catch((err) => { 101 | switch (err.message) { 102 | case 'DB_ACCESS_ERROR': 103 | console.log(`cannot write to DB: ${DATABASE_NAME}`); 104 | break; 105 | case 'DB_SERIALIZE_ERROR': 106 | console.log(`cannot serialize contents as JSON`); 107 | break; 108 | } 109 | }); 110 | */ 111 | 112 | xapi.on('ready', async () => { 113 | await write({ player : 'steve', score: 5 }); 114 | let data = await read(); 115 | console.log(`player score is: ${data.score}`) 116 | }); 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /macros/15-cipher.js: -------------------------------------------------------------------------------- 1 | // 2 | // Extract from https://stackoverflow.com/questions/18279141/javascript-string-encryption-and-decryption 3 | // Contributed by https://stackoverflow.com/users/2861702/jorgeblom 4 | // 5 | 6 | /* 7 | * Simplist symetric encoding using a shared secret 8 | * 9 | */ 10 | const cipher = salt => { 11 | const textToChars = text => text.split('').map(c => c.charCodeAt(0)); 12 | const byteHex = n => ("0" + Number(n).toString(16)).substr(-2); 13 | const applySaltToChar = code => textToChars(salt).reduce((a,b) => a ^ b, code); 14 | 15 | return text => text.split('') 16 | .map(textToChars) 17 | .map(applySaltToChar) 18 | .map(byteHex) 19 | .join(''); 20 | } 21 | 22 | const decipher = salt => { 23 | const textToChars = text => text.split('').map(c => c.charCodeAt(0)); 24 | const applySaltToChar = code => textToChars(salt).reduce((a,b) => a ^ b, code); 25 | return encoded => encoded.match(/.{1,2}/g) 26 | .map(hex => parseInt(hex, 16)) 27 | .map(applySaltToChar) 28 | .map(charCode => String.fromCharCode(charCode)) 29 | .join(''); 30 | } 31 | 32 | // Example 33 | const text = 'Vision without execution is hallucination'; 34 | console.log(`BEFORE: ${text}`); 35 | const encrypted = cipher('not so secret')(text); 36 | console.log(`ENCRYPTED: ${encrypted}`); 37 | 38 | const decrypted = decipher('not so secret')(encrypted); 39 | console.log(`DECRYPTED: ${decrypted}`); 40 | 41 | if (text !== decrypted) { 42 | console.error('failed'); 43 | } -------------------------------------------------------------------------------- /macros/17-persistenv.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | // An advanced implementation for the Environment Macro that persists ENV values to the local device (using an 'ENV' Macro to serialize JSON) 7 | 8 | const xapi = require('xapi'); 9 | 10 | // Read ENV from local storage 11 | const DEFAULT_ENV = {PING:'PONG'}; 12 | let ENV; 13 | xapi.on('ready', async () => { 14 | let data = await read(); 15 | 16 | // if env is empty, create a new storage with default env 17 | if (!data || (!data.PING)) { 18 | console.info('No existing ENV, creating default'); 19 | ENV = DEFAULT_ENV; 20 | await write(ENV); 21 | } 22 | else { 23 | ENV = data; 24 | } 25 | }); 26 | 27 | xapi.event.on('Message Send Text', (msg) => { 28 | console.debug(`new "Message Send" Event with text: ${msg}`); 29 | 30 | // WORKAROUND: needed if message was sent from the cloud via POST https://api.ciscospark.com/v1/xapi/command/message.send 31 | msg = msg.replace(/\'/g, '"'); 32 | let parsed; 33 | try { 34 | parsed = JSON.parse(msg); 35 | } 36 | catch (err) { 37 | console.err(`cannot JSON parse the text in 'Message Send' Event: ${msg}, ignoring...`); 38 | return 39 | } 40 | 41 | let data = parsed; 42 | if (!data.env) { 43 | console.debug(`"Message Send" Event is not about Env: ${msg}, ignoring`); 44 | return; 45 | } 46 | 47 | console.debug(`"Message Sent" event concerns env variable: "${data.env}"`); 48 | 49 | // GET ENV 50 | if (data.operation && (data.operation == 'get')) { 51 | 52 | if (!data.env) { 53 | console.warn(`no ENV variable specified in: "${msg}", ignoring...`); 54 | return; 55 | } 56 | 57 | console.debug(`requested value for env variable: "${data.env}"`); 58 | let response = { 59 | 'env': data.env, 60 | 'operation': 'get_response', 61 | 'value': '' // default to '' if variable is not found 62 | } 63 | let value = ENV[data.env]; 64 | if (value) { 65 | response.value = value; 66 | } 67 | 68 | // Publish value for ENV variable 69 | xapi.command('Message Send', { Text: JSON.stringify(response) }); 70 | return; 71 | } 72 | 73 | // RESPONSE provided => ignore 74 | if (data.operation && (data.operation == 'get_response')) { 75 | console.debug(`ignoring get_response type of "Message Sent" event: ${msg}`); 76 | return; 77 | } 78 | 79 | // SET ENV 80 | // Note that the storage is volatile (dynamically added variables will be lost every time the macro or runtime is restared) 81 | if (data.operation && (data.operation == 'set')) { 82 | 83 | console.log(`setting value: "${data.value}" for env variable: "${data.env}"`); 84 | ENV[data.env] = data.value; 85 | 86 | // Write to local storage 87 | try { 88 | write(ENV); 89 | } 90 | catch(err) { 91 | console.warn(`could not write variable to ENV, err: ${err.message}`) 92 | } 93 | return; 94 | } 95 | 96 | console.warn(`operation for "Message Sent" event is not supported:"${msg}, ignoring...`); 97 | return; 98 | }) 99 | 100 | 101 | const PREFIX = 'const json = '; 102 | const DATABASE_NAME = 'ENV'; // name of the macro with db contents 103 | 104 | // Read database contents 105 | async function read() { 106 | // Load contents 107 | let contents; 108 | try { 109 | let macro = await xapi.command('Macros Macro Get', { Name: DATABASE_NAME, Content: true }) 110 | contents = macro.Macro[0].Content.substring(PREFIX.length); 111 | } 112 | catch(err) { 113 | console.error(`cannot load contents from macro: ${DATABASE_NAME}`); 114 | throw new Error("DB_ACCESS_ERROR"); 115 | } 116 | 117 | // Parse contents 118 | try { 119 | console.debug(`DB contains: ${contents}`); 120 | let data = JSON.parse(contents); 121 | console.debug('DB successfully parsed'); 122 | return data; 123 | } 124 | catch (err) { 125 | console.error('DB is corrupted, cannot JSON parse the DB'); 126 | throw new Error('DB_PARSE_ERROR'); 127 | } 128 | } 129 | 130 | // Write database contents 131 | async function write(data) { 132 | // Serialize data as JSON and append prefix 133 | let contents; 134 | try { 135 | contents = PREFIX + JSON.stringify(data); 136 | } 137 | catch (err) { 138 | console.debug('Contents cannot be serialized to JSON'); 139 | throw new Error('DB_SERIALIZE_ERROR'); 140 | } 141 | 142 | // Write 143 | try { 144 | let res = await xapi.command('Macros Macro Save', { Name: DATABASE_NAME, OverWrite: true, body: contents }); 145 | return (res.status == 'OK'); 146 | } 147 | catch (err) { 148 | console.error(`cannot write contents to macro: ${DATABASE_NAME}`); 149 | throw new Error('DB_ACCESS_ERROR'); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /macros/18-hue_disco.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /* 7 | * This macro will enlighten your meeting room, 8 | * no complementary In-Room control needed: 9 | * 10 | * pre-req: 11 | * - configure the macro with your Hue Bridge info and you're good to go. 12 | * - configure your device for HttpClient 13 | * 14 | */ 15 | 16 | const xapi = require('xapi'); 17 | 18 | xapi.on('ready', () => { 19 | disco1(1000, false); 20 | //disco2(0, 1000, false); 21 | }); 22 | 23 | // Blinks at a regular delay, with changing color 24 | // example: disco1(1000, false); 25 | 26 | function disco1(delay, black) { 27 | let color = COLOR_BLACK; 28 | if (!black) { 29 | color = { 30 | hue: Math.round(Math.random() * 65535), 31 | on: true, 32 | sat: 255 33 | }; 34 | } 35 | 36 | // comment this line to remove blinking effect 37 | black = !black; 38 | changeColor(color); 39 | 40 | const MIN_DELAY = 300; 41 | setTimeout(function () { 42 | disco1(delay + MIN_DELAY, black); 43 | }, delay); 44 | } 45 | 46 | // Blinks at random delays and random colors 47 | // example: disco2(300, 1000, false); 48 | function disco2(delay, maxDelay, black) { 49 | // Randomize color 50 | let color = COLOR_BLACK; 51 | if (delay < 500 || !black) { 52 | color = { 53 | hue: Math.round(Math.random() * 65535), 54 | on: true, 55 | sat: 255 56 | }; 57 | } 58 | 59 | // comment this line to remove blinking effect 60 | black = !black; 61 | changeColor(color); 62 | 63 | // Randomize delay 64 | const MIN_DELAY = 300; 65 | let nextDelay = Math.round(Math.random() * maxDelay) + MIN_DELAY; 66 | setTimeout(function () { 67 | disco2(nextDelay, maxDelay + MIN_DELAY, black); 68 | }, delay); 69 | } 70 | 71 | 72 | // 73 | // Hue Library 74 | // 75 | 76 | // ACTION: Configure for your Philips Hue deployment 77 | const HUE_BRIDGE = '192.168.1.33'; 78 | const HUE_USERNAME = 'EM2Vg2GtNUqAASukv47wm1pWY0FayFe48D03f6Cb'; 79 | const HUE_LIGHT = 1; // number of the light in your deployment 80 | 81 | console.info(`Hue Bridge with ip: ${HUE_BRIDGE}, bulb: ${HUE_LIGHT}`); 82 | const hue_url = `http://${HUE_BRIDGE}/api/${HUE_USERNAME}/lights/${HUE_LIGHT}/state`; 83 | 84 | const COLOR_RED = { on: true, hue: 65535, sat: 255 }; 85 | const COLOR_BLUE = { on: true, hue: 46920, sat: 255 }; 86 | const COLOR_GREEN = { on: true, hue: 25500, sat: 255 }; 87 | const COLOR_WHITE = { on: true, hue: 0, sat: 0 }; 88 | const COLOR_BLACK = { on: false }; 89 | 90 | function changeColor(color) { 91 | 92 | console.debug(`posting to hue bulb: ${JSON.stringify(color)}`) 93 | 94 | // Post message 95 | xapi.command( 96 | 'HttpClient Put', 97 | { 98 | Header: ["Content-Type: application/json"], 99 | Url: hue_url, 100 | AllowInsecureHTTPS: "True" 101 | }, 102 | JSON.stringify(color)) 103 | .catch((err) => { 104 | console.error('could not contact the Hue Bridge'); 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /macros/19-hue_onair.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /* 7 | * Turns red a designated Hue bulb when a call is in progress, 8 | * and keep green when out of call. 9 | * 10 | * pre-req: 11 | * - configure the macro with your Hue Bridge info and you're good to go. 12 | * - configure your device for HttpClient 13 | * 14 | */ 15 | 16 | const xapi = require('xapi'); 17 | 18 | // 19 | // Hue Library 20 | // 21 | 22 | // ACTION: Update for your Philips Hue deployment 23 | const HUE_BRIDGE = '192.168.1.33'; 24 | const HUE_USERNAME = 'EM2Vg2GtNUqAASukv47wm1pWY0FayFe48D03f6Cb'; 25 | const HUE_LIGHT = 1; // number of the light in your deployment 26 | 27 | console.info(`Hue Bridge with ip: ${HUE_BRIDGE}, bulb: ${HUE_LIGHT}`); 28 | const hue_url = `http://${HUE_BRIDGE}/api/${HUE_USERNAME}/lights/${HUE_LIGHT}/state`; 29 | 30 | const COLOR_RED = { on: true, hue: 65535, sat: 255}; 31 | const COLOR_BLUE = { on: true, hue: 46920, sat: 255}; 32 | const COLOR_GREEN = { on: true, hue: 25500, sat: 255}; 33 | const COLOR_WHITE = { on: true, hue: 0, sat: 0}; 34 | const COLOR_BLACK = { on: false}; 35 | function changeColor(color) { 36 | 37 | // Post message 38 | xapi.command('HttpClient Put', { 39 | Header: ["Content-Type: application/json"], 40 | Url: hue_url, 41 | AllowInsecureHTTPS: "True" 42 | }, 43 | JSON.stringify(color)) 44 | .catch((err) => { 45 | console.error('could not contact the Hue Bridge'); 46 | }); 47 | } 48 | 49 | // 50 | // Macro 51 | // 52 | 53 | xapi.on('ready', init); 54 | 55 | function init() { 56 | // Green at launch 57 | changeColor(COLOR_GREEN); 58 | 59 | // Listen to active calls and update color accordingly 60 | xapi.status.on("SystemUnit State NumberOfActiveCalls", (activeCalls) => { 61 | console.debug(`NumberOfActiveCalls is: ${activeCalls}`) 62 | 63 | // Turn red if call in progress 64 | if (activeCalls > 0) { 65 | console.info('call in progress'); 66 | changeColor(COLOR_RED); 67 | return; 68 | } 69 | 70 | // Turn green if out of call 71 | console.log('no active call'); 72 | changeColor(COLOR_GREEN); 73 | }); 74 | 75 | 76 | } -------------------------------------------------------------------------------- /macros/2-showVolume.js: -------------------------------------------------------------------------------- 1 | const xapi = require('xapi'); 2 | 3 | xapi.status 4 | .get('Audio Volume') 5 | .then((volume) => { console.log(volume); }) -------------------------------------------------------------------------------- /macros/20-import_main.js: -------------------------------------------------------------------------------- 1 | // utils macro 2 | import xapi from 'xapi'; 3 | 4 | export async function getVolume() { 5 | return await xapi.Status.Audio.Volume.get(); 6 | } 7 | 8 | export function sleepFor(timeout) { 9 | return new Promise((resolve) => { 10 | setTimeout(resolve, timeout); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /macros/20-import_util.js: -------------------------------------------------------------------------------- 1 | // main macro 2 | import { getVolume, sleepFor } from './import_utils'; 3 | 4 | async function main() { 5 | const timeout = 2000; 6 | console.log(`fetching volume + sleeping for ${timeout}ms`); 7 | 8 | const [volume] = await Promise.all([ 9 | getVolume(), 10 | sleepFor(timeout), 11 | ]); 12 | console.log(`DONE: volume is ${volume}`); 13 | } 14 | 15 | main(); 16 | -------------------------------------------------------------------------------- /macros/3-changeLanguage.js: -------------------------------------------------------------------------------- 1 | const xapi = require('xapi'); 2 | 3 | 4 | function onGui(event) { 5 | 6 | console.log("new event"); 7 | 8 | if ((event.Type == 'clicked') && (event.WidgetId == 'toFrench')) { 9 | xapi.config.set('UserInterface Language', 'French'); 10 | } 11 | 12 | if ((event.Type == 'clicked') && (event.WidgetId == 'toEnglish')) { 13 | xapi.config.set('UserInterface Language', 'English'); 14 | } 15 | } 16 | 17 | 18 | xapi.event.on('UserInterface Extensions Widget Action', onGui); 19 | -------------------------------------------------------------------------------- /macros/4-webhook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sends HTTP payload everytime People Analytics are pushed 3 | */ 4 | const xapi = require('xapi'); 5 | 6 | xapi.command('HttpFeedback Register', { 7 | FeedbackSlot: 2, 8 | ServerUrl: "https://requestb.in/szua2usz", 9 | Format: "JSON", 10 | Expression: ["/Status/RoomAnalytics/PeoplePresence", "/Status/RoomAnalytics/PeopleCount"] 11 | }) 12 | .then(console.log("successfully registered Webhook for PeoplePresence & PeopleCount")) 13 | .catch(console.error); -------------------------------------------------------------------------------- /macros/4b-webhook-controls.js: -------------------------------------------------------------------------------- 1 | const xapi = require('xapi'); 2 | 3 | xapi.command('HttpFeedback Register', { 4 | FeedbackSlot: 2, 5 | ServerUrl: "https://requestb.in/156r6651", 6 | Format: "JSON", 7 | Expression: ["/Event/UserInterface/Extensions/Widget/Action"] 8 | }) 9 | .then(console.log("successfully registered Webhook for In-Room controls")) 10 | .catch(console.error); 11 | -------------------------------------------------------------------------------- /macros/5-checkEmail.js: -------------------------------------------------------------------------------- 1 | function isEmail(email) { 2 | // extract from http://stackoverflow.com/questions/46155/validate-email-address-in-javascript 3 | let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 4 | return re.test(email); 5 | } 6 | 7 | let email1 = "stsfartz@cisco" 8 | console.log(`is an email? ${email1}: ${isEmail(email1)}`) 9 | 10 | let email2 = "stsfartz@cisco.com" 11 | console.log(`is an email? ${email2}: ${isEmail(email2)}`) 12 | -------------------------------------------------------------------------------- /macros/7-showProgress.js: -------------------------------------------------------------------------------- 1 | function showProgress(count, delay, cb, final, initial) { 2 | let counter = 0; 3 | if (!initial) initial = ""; 4 | let progress = ".....".padEnd(count, '.'); 5 | setInterval(function (event) { 6 | cb(initial + progress.substring(0, counter + 1)); 7 | counter++; 8 | if (counter >= count) { 9 | clearInterval(timer); 10 | cb(final); 11 | } 12 | }, delay); 13 | } 14 | 15 | showProgress(20, 100, console.log, "completed", "working"); 16 | -------------------------------------------------------------------------------- /macros/8-message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Illustrates how to pass messages among local macros 3 | * 4 | * see 12-primary and 12-secondary examples for communication through remote codecs 5 | */ 6 | 7 | const xapi = require('xapi'); 8 | 9 | 10 | xapi.event.on("Message Send Text", (text) => { 11 | 12 | // Decode if necessary (example: message sent over xAPI cloud) 13 | let decoded = text.replace(/\'/g, '"'); 14 | 15 | // Parse JSON and print 16 | let data = JSON.parse(decoded); 17 | console.log(`Received score: ${data.score}, for player: ${data.player}`); 18 | }); 19 | 20 | 21 | let data = { 22 | score: 5, 23 | player: "Stève" 24 | } 25 | 26 | // Serialize as JSON 27 | const serialized = JSON.stringify(data); 28 | 29 | // [CONFIRM] Encoding is needed only if sending over HTTP 30 | const encoded = serialized.replace(/"/g, "\'"); 31 | 32 | xapi.command("Message Send", { Text: encoded }); -------------------------------------------------------------------------------- /macros/9-timer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Illustrates a personalized message coupled with a timer 3 | * 4 | */ 5 | 6 | const xapi = require('xapi'); 7 | 8 | 9 | function update(value) { 10 | // 11 | // Custom logic 12 | // 13 | console.log(value); 14 | } 15 | 16 | let current = {}; 17 | current.options = []; 18 | current.iterations = 0; 19 | current.iterate = function (original, max) { 20 | // Reset options if array is empty 21 | if (this.options.length === 0) { 22 | this.options = original; 23 | } 24 | 25 | this.iterations++; 26 | if (this.iterations > max) { 27 | clearInterval(current.timer); 28 | return; 29 | } 30 | 31 | // Pop next option 32 | let choice = this.options.pop(); 33 | update(choice) 34 | } 35 | 36 | current.timer = setInterval(function () { 37 | current.iterate(["Read a tutorial", "Launch a Sandbox", "Attend an event", "Pick a DevNet activity among:"], 10); 38 | }, 1000); 39 | 40 | -------------------------------------------------------------------------------- /macros/README.md: -------------------------------------------------------------------------------- 1 | # Javascript Macros 2 | 3 | Read through the code of these macros to quickly ramp up with the xAPI of your Room Devices. 4 | 5 | Then, reach to the [controls](../controls) to learn more of your Room Device programmability. 6 | 7 | **New to Controls & Macros? check the [QuickStart Guide](../docs/QuickStart.md) to learn to load Controls and Macros to your device** 8 | -------------------------------------------------------------------------------- /macros/pdf/macro-tutorial.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/xapi-samples/4457a1c4f9f8c6715830fae314b0b721cc612f3f/macros/pdf/macro-tutorial.pdf -------------------------------------------------------------------------------- /python/.gitignore: -------------------------------------------------------------------------------- 1 | env/ 2 | -------------------------------------------------------------------------------- /python/advanced.py: -------------------------------------------------------------------------------- 1 | # Extract from xows article 2 | # https://community.cisco.com/t5/collaboration-voice-and-video/xapi-over-websocket-xows-ce9-7-x/ba-p/3831553 3 | 4 | import websockets 5 | import ssl 6 | import asyncio 7 | import base64 8 | import json 9 | 10 | count = 0 11 | 12 | async def connect(): 13 | return await websockets.connect('wss://{}/ws/'.format('10.10.10.1'), ssl=ssl._create_unverified_context(), extra_headers={ 14 | 'Authorization': 'Basic {}'.format(base64.b64encode('{}:{}'.format('admin', '').encode()).decode('utf-8'))}) 15 | 16 | def construct(method): 17 | global count 18 | count += 1 19 | return {'jsonrpc': '2.0', 'id': str(count), 'method': method} 20 | 21 | def query(params): 22 | payload = construct('xQuery') 23 | payload['params'] = {'Query': params.split()} 24 | return payload 25 | 26 | def get(params): 27 | payload = construct('xGet') 28 | params = [i if not i.isnumeric() else int(i) for i in params.split()] 29 | payload['params'] = {'Path': params} 30 | return payload 31 | 32 | def command(path, params=None): 33 | payload = construct('{}{}'.format('xCommand/', '/'.join(path.split(' ')))) 34 | 35 | # Params are for multiline commands and other command parameters {'ConfigId':'example', 'body':'1.0...'} 36 | if params != None: 37 | payload['params'] = params 38 | 39 | return payload 40 | 41 | def config(path, value): 42 | payload = construct('xSet') 43 | payload['params'] = { 44 | "Path": ['Configuration'] + path.split(' '), 45 | "Value": value 46 | } 47 | return payload 48 | 49 | def feedbackSubscribe(path=None, notify=False): 50 | payload = construct('xFeedback/Subscribe') 51 | payload['params'] = { 52 | "Query": path.split(' '), 53 | "NotifyCurrentValue": notify 54 | } 55 | return payload 56 | 57 | async def send(ws, message): 58 | await ws.send(json.dumps(message)) 59 | print('Sending:', message) 60 | 61 | async def receive(ws): 62 | result = await ws.recv() 63 | print('Receive:', result) 64 | 65 | async def task(): 66 | ws = await connect() 67 | try: 68 | await send(ws, get('Status SystemUnit Uptime')) 69 | await receive(ws) 70 | finally: 71 | asyncio.run(ws.close()) 72 | 73 | asyncio.run(task()) -------------------------------------------------------------------------------- /python/requirements.txt: -------------------------------------------------------------------------------- 1 | websockets==8.1 2 | -------------------------------------------------------------------------------- /python/websocket.py: -------------------------------------------------------------------------------- 1 | # Extract from xows article 2 | # https://community.cisco.com/t5/collaboration-voice-and-video/xapi-over-websocket-xows-ce9-7-x/ba-p/3831553 3 | 4 | import websockets 5 | import ssl 6 | import asyncio 7 | import base64 8 | 9 | 10 | async def connect(): 11 | return await websockets.connect('wss://{}/ws/'.format('192.168.1.3'), ssl=ssl._create_unverified_context(), extra_headers={ 12 | 'Authorization': 'Basic {}'.format(base64.b64encode('{}:{}'.format('localadmin', 'ciscopsdt').encode()).decode('utf-8'))}) 13 | 14 | 15 | async def send(ws, message): 16 | await ws.send(message) 17 | print('Sending:', message) 18 | 19 | 20 | async def receive(ws): 21 | result = await ws.recv() 22 | print('Receive:', result) 23 | 24 | 25 | async def task(): 26 | try: 27 | ws = await connect() 28 | except TimeoutError as timeout: 29 | print('could not connect to device, error: ', timeout) 30 | else: 31 | try: 32 | await send(ws, '{"jsonrpc": "2.0","id": "0","method": "xGet","params": {"Path": ["Status", "SystemUnit", "State"]}}') 33 | await receive(ws) 34 | finally: 35 | await ws.close() 36 | 37 | asyncio.run(task()) 38 | -------------------------------------------------------------------------------- /pyxows/README.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Before running any of these examples, you need to first install the 'xows' package on your local machine. 4 | 5 | To add the 'xows' package to your environment, type: 6 | 7 | > Note: by default, the commands below adds the 'xows' package (e.g. xows-1.0.2-py3.7.egg) to the site packages of the current user. Use the following command to install the package for another user `python setup.py install [--user]` 8 | 9 | ```bash 10 | git clone https://github.com/cisco-ce/pyxows 11 | cd pyxows 12 | python setup.py install 13 | ``` 14 | 15 | 16 | To check the 'xows' package is installed on your machine, type: 17 | 18 | ```bash 19 | pip show xows 20 | ``` 21 | -------------------------------------------------------------------------------- /pyxows/connection_timeout.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Cisco Systems 2 | # Licensed under the MIT License 3 | 4 | # 5 | # Script to keep the audio volume level set to a maximum value 6 | # 7 | 8 | import xows 9 | import aiohttp 10 | import asyncio 11 | 12 | 13 | async def task(): 14 | try: 15 | async with xows.XoWSClient('192.168.1.3', username='localadmin', password='ciscopsdt') as client: 16 | def callback(data, id): 17 | print(f'New event received with id: ({id}): {data}') 18 | 19 | print('Created subscription with number: ', 20 | await client.subscribe(['Status', 'Audio', 'Volume'], callback, True)) 21 | 22 | await client.wait_until_closed() 23 | 24 | except xows.XoWSError as error: 25 | print('Timeout, could not connect to device. Exiting...') 26 | print('error', error) 27 | except TimeoutError: 28 | print('TimeoutError') 29 | except aiohttp.client_exceptions.ClientConnectorError: 30 | print('ClientConnectorError') 31 | 32 | 33 | try: 34 | asyncio.run(task()) 35 | except KeyboardInterrupt: 36 | print('Interrupted, exiting...') 37 | -------------------------------------------------------------------------------- /pyxows/control_volume.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Cisco Systems 2 | # Licensed under the MIT License 3 | 4 | # 5 | # Control the volume of a CE-powered device 6 | # 7 | 8 | import xows 9 | import asyncio 10 | import os 11 | 12 | async def control_volume(device_url, user, passwd): 13 | 14 | async with xows.XoWSClient(url_or_host=device_url, username=user, password=passwd) as client: 15 | 16 | MAX_VOLUME = 60 17 | def callback(event, id): 18 | if id == callback_id: 19 | volume = event['Status']['Audio']['Volume'] 20 | print(f'Volume currently set at level: {volume}') 21 | 22 | if volume > MAX_VOLUME: 23 | print(f'Volume above max authorized, turning down to: {MAX_VOLUME}') 24 | asyncio.create_task(client.xCommand(['Audio', 'Volume', 'Set'], Level=MAX_VOLUME)) 25 | 26 | callback_id = await client.subscribe(['Status', 'Audio', 'Volume'], callback, True) 27 | 28 | await client.wait_until_closed() 29 | 30 | try: 31 | device = os.environ.get('DEVICE_IP', default='192.168.1.32') 32 | user = os.environ.get('DEVICE_USER', default='localadmin') 33 | passwd = os.environ.get('DEVICE_PASSWD', default='ciscopsdt') 34 | asyncio.run(control_volume(device, user, passwd)) 35 | except AttributeError: 36 | print('invalid credentials') 37 | except KeyboardInterrupt: 38 | print('exiting') 39 | -------------------------------------------------------------------------------- /pyxows/usage.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Cisco Systems 2 | # Licensed under the MIT License 3 | 4 | # 5 | # Usage sample from README.md at https://github.com/cisco-ce/pyxows 6 | # 7 | 8 | import xows 9 | import asyncio 10 | 11 | async def start(): 12 | async with xows.XoWSClient(url_or_host='192.168.1.32', username='localadmin', password='ciscopsdt') as client: 13 | def callback(data, id_): 14 | print(f'Feedback (Id {id_}): {data}') 15 | 16 | print('Status Query:', 17 | await client.xQuery(['Status', '**', 'DisplayName'])) 18 | 19 | print('Get:', 20 | await client.xGet(['Status', 'Audio', 'Volume'])) 21 | 22 | print('Command:', 23 | await client.xCommand(['Audio', 'Volume', 'Set'], Level=60)) 24 | 25 | print('Configuration:', 26 | await client.xSet(['Configuration', 'Audio', 'DefaultVolume'], 50)) 27 | 28 | print('Subscription 1:', 29 | await client.subscribe(['Status', 'Audio', 'Volume'], callback, True)) 30 | 31 | await client.wait_until_closed() 32 | 33 | asyncio.run(start()) -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # xAPI from Web pages 2 | 3 | To invoke xAPI from an HTML page, you first need to generate a packaged distribution for the jsxapi. 4 | 5 | From a bash terminal, run the commands below to generate a `jsxapi.min.js` file: 6 | 7 | ```shell 8 | git clone https://github.com/cisco-ce/jsxapi 9 | cd jsxapi 10 | npm run build:dist:min 11 | cd dist 12 | ``` 13 | 14 | -------------------------------------------------------------------------------- /web/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | xAPI from a Web Page 8 | 9 | 10 | 11 | 12 |
    13 |
    14 |
    username:
    15 |
    password:
    16 |

    17 | 18 | 19 |
    20 | 21 | 22 | 42 | 43 | 44 | --------------------------------------------------------------------------------