├── .eslintrc.json ├── .gitignore ├── .idea ├── encodings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jsLibraryMappings.xml ├── misc.xml ├── modules.xml ├── preferred-vcs.xml ├── telepresenceCommands.iml ├── vcs.xml ├── watcherTasks.xml └── workspace.xml ├── LICENSE.md ├── README.md ├── endpointLogs ├── Thu, 29 Mar 2018 16:01:31 GMT.x-gzip └── readme.txt ├── endpoints └── endpoints.csv ├── gulpfile.js ├── img ├── brand │ └── expedia.jpg ├── oldImages │ ├── expedia.jpg │ └── kaiser-permanente272.jpg └── wallpaper │ └── Kaiser-Permanente-Exterior.jpg ├── package.json ├── server.js ├── svrConfig ├── comFunct.js ├── httpServer.js └── logger.js ├── tools ├── buildXml.js ├── collectLogs.js ├── endpoint.js ├── excel.js ├── fileWatcher.js ├── image64.js ├── ipaddress.js └── tpXapi.js └── xmlFiles ├── backupBundle └── backup-bundle.zip ├── macros └── proximityMacro.js ├── readme.txt └── roomControls └── proximityButton.xml /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:import/errors", 6 | "plugin:import/warnings" 7 | ], 8 | "parserOptions": { 9 | "ecmaVersion": 7, 10 | "sourceType": "module" 11 | }, 12 | "env": { 13 | "browser": true, 14 | "node": true, 15 | "mocha": true 16 | }, 17 | "rules": { 18 | "no-console": 0 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Cloud 9 files 2 | .c9/ 3 | # Space date 4 | space.json 5 | cart.json 6 | #client websocket file 7 | device.json 8 | #access log 9 | access.log 10 | /endpointLogs 11 | 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # Typescript v1 declaration files 51 | typings/ 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .idea/ 71 | 72 | # User-specific stuff: 73 | .idea/**/workspace.xml 74 | .idea/**/tasks.xml 75 | .idea/dictionaries 76 | 77 | # Sensitive or high-churn files: 78 | .idea/**/dataSources/ 79 | .idea/**/dataSources.ids 80 | .idea/**/dataSources.local.xml 81 | .idea/**/sqlDataSources.xml 82 | .idea/**/dynamic.xml 83 | .idea/**/uiDesigner.xml 84 | 85 | # Gradle: 86 | .idea/**/gradle.xml 87 | .idea/**/libraries 88 | 89 | # CMake 90 | cmake-build-debug/ 91 | cmake-build-release/ 92 | 93 | # Mongo Explorer plugin: 94 | .idea/**/mongoSettings.xml 95 | 96 | ## File-based project format: 97 | *.iws 98 | 99 | ## Plugin-specific files: 100 | 101 | # IntelliJ 102 | out/ 103 | 104 | # mpeltonen/sbt-idea plugin 105 | .idea_modules/ 106 | 107 | # JIRA plugin 108 | atlassian-ide-plugin.xml 109 | 110 | # Cursive Clojure plugin 111 | .idea/replstate.xml 112 | 113 | # Crashlytics plugin (for Android Studio and IntelliJ) 114 | com_crashlytics_export_strings.xml 115 | crashlytics.properties 116 | crashlytics-build.properties 117 | fabric.properties -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | true 8 | 9 | false 10 | true 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/preferred-vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ApexVCS 5 | 6 | -------------------------------------------------------------------------------- /.idea/telepresenceCommands.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | conn 144 | connect 145 | cart 146 | _ 147 | console 148 | 149 | 150 | 151 | 153 | 154 | 198 | 199 | 200 | 201 | 202 | true 203 | DEFINITION_ORDER 204 | 205 | 206 | 207 | 208 | 209 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 238 | 239 | 242 | 243 | 244 | 245 | 248 | 249 | 252 | 253 | 256 | 257 | 258 | 259 | 262 | 263 | 266 | 267 | 270 | 271 | 274 | 275 | 276 | 277 | 280 | 281 | 284 | 285 | 288 | 289 | 290 | 291 | 294 | 295 | 298 | 299 | 302 | 303 | 304 | 305 | 308 | 309 | 312 | 313 | 316 | 317 | 320 | 321 | 322 | 323 | 326 | 327 | 330 | 331 | 334 | 335 | 336 | 337 | 340 | 341 | 344 | 345 | 348 | 349 | 352 | 353 | 354 | 355 | 358 | 359 | 362 | 363 | 366 | 367 | 368 | 369 | 372 | 373 | 376 | 377 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 466 | 467 | project 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | project 484 | 485 | 486 | true 487 | 488 | 489 | 490 | DIRECTORY 491 | 492 | false 493 | 494 | 495 | 496 | 497 | 499 | 500 | 501 | 502 | 1515446082004 503 | 517 | 518 | 1520978863692 519 | 524 | 525 | 1520979009241 526 | 531 | 532 | 1520979214613 533 | 538 | 539 | 1520979357067 540 | 545 | 546 | 1520979618282 547 | 552 | 553 | 1520980628484 554 | 559 | 560 | 1521053380436 561 | 566 | 567 | 1521060143254 568 | 573 | 574 | 1522096094040 575 | 580 | 581 | 1522165475672 582 | 587 | 588 | 1522257603638 589 | 594 | 595 | 1522260734853 596 | 601 | 602 | 1522261082359 603 | 608 | 609 | 1522699920928 610 | 615 | 616 | 1522700698832 617 | 622 | 623 | 1522702769195 624 | 629 | 632 | 633 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 685 | 686 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 947 | 948 | 949 | 950 | 951 | 952 | 953 | 954 | 955 | 956 | 957 | 958 | 959 | 960 | 961 | 962 | 963 | 964 | 965 | 966 | 967 | 968 | 969 | 970 | 971 | 972 | 973 | 974 | 975 | 976 | 977 | 978 | 979 | 980 | 981 | 982 | 983 | 984 | 985 | 986 | 987 | 988 | 989 | 990 | 991 | 992 | 993 | 994 | 995 | 996 | 997 | 998 | 999 | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | 1013 | 1014 | 1015 | 1016 | 1017 | 1018 | 1019 | 1020 | 1021 | 1022 | 1023 | 1024 | 1025 | 1026 | 1027 | 1028 | 1029 | 1030 | 1031 | 1032 | 1033 | 1034 | 1035 | 1036 | 1037 | 1038 | 1039 | 1040 | 1041 | 1042 | 1043 | 1044 | 1045 | 1046 | 1047 | 1048 | 1049 | 1050 | 1051 | 1052 | 1053 | 1054 | 1055 | 1056 | 1057 | 1058 | 1059 | 1060 | 1061 | 1062 | 1063 | 1064 | 1065 | 1066 | 1067 | 1068 | 1069 | 1070 | 1071 | 1072 | 1073 | 1074 | 1075 | 1076 | 1077 | 1078 | 1079 | 1080 | 1081 | 1082 | 1083 | 1084 | 1085 | 1086 | 1087 | 1088 | 1089 | 1090 | 1091 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telepresence Deploy XML APP 2 | 3 | Simple app to deploy packages to Cisco Telepresence apps. 4 | Branding for CE9.2.1+ and backup bundles for CE9.3+ devices is supported. Backup bundles is a new features for CE9.3+ firmware. 5 | 6 | Currently capable of deploying: 7 | * Custom Branding with little fuss. Checks the endpoint if its branding capable via firmware version. Takes care of reading CSV files for endpoints and also base64 encoding of image files. 8 | * Using the branding option the script will check your endpoint version and deploy branding to endpoints capable or wallpaper for non-branding capable devices. SX10 check is supported. SX10 has no branding option even if Firmware is CE9.3. 9 | * Deploy wallpaper images (also disables branding)to all your endpoints instead of using the branding option. 10 | * Backup bundle to multiple endpoints. Will create the backup bundle checksum for deployment and acts as http server for package delivery. 11 | 12 | ## Getting Started 13 | 14 | The following applications and hardware are required: 15 | 16 | 17 | * Cisco Video endpoint 18 | * Nodejs 19 | * CSV file with IP addresses for endpoints placed in the Endpoint directory 20 | * Image files to be deployed placed in branding and wallpaper directories 21 | * Branding image 272x272 preferred 22 | * Background Image 1920x1080 preferred 23 | * Backup bundle created using CE9.3 device 24 | ### Prerequisites 25 | 26 | Configuration required: 27 | 28 | * Video endpoint 29 | 30 | 31 | ### Installing 32 | 33 | #### Via Git 34 | ```bash 35 | mkdir myproj 36 | cd myproj 37 | git clone https://github.com/voipnorm/CiscoTPCustomXML.git 38 | npm install 39 | 40 | ``` 41 | 42 | Set the following environment variables in a .env file... 43 | 44 | ``` 45 | PORT= 46 | TPADMIN= 47 | TPADMINPWD= 48 | 49 | ``` 50 | ## Running Script 51 | To run the script use one of the following commands: 52 | ``` 53 | node server.js branding 54 | ``` 55 | or 56 | ``` 57 | node server.js bundle 58 | ``` 59 | To use the bundle command ensure you have created a backup bundle from your CE device and placed the zip file into: 60 | ``` 61 | ./xmlFiles/backupBundle 62 | ``` 63 | To deploy wallpaper: 64 | ``` 65 | node server.js wallpaper 66 | ``` 67 | Ensure wallpaper image is available in ./img/wallpaper directory. 68 | ## Built With 69 | 70 | * Nodejs 71 | 72 | ## License 73 | 74 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 75 | 76 | ## Acknowledgments 77 | 78 | * Me 79 | 80 | -------------------------------------------------------------------------------- /endpointLogs/Thu, 29 Mar 2018 16:01:31 GMT.x-gzip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voipnorm/CiscoTPCustomXML/f603f3b40f6093c01faaaaf85f143382e2323615/endpointLogs/Thu, 29 Mar 2018 16:01:31 GMT.x-gzip -------------------------------------------------------------------------------- /endpointLogs/readme.txt: -------------------------------------------------------------------------------- 1 | downloaded endpoint logs will apear in this directory. 2 | 3 | Use "node server.js logs " -------------------------------------------------------------------------------- /endpoints/endpoints.csv: -------------------------------------------------------------------------------- 1 | 10.27.42.116 2 | 10.27.42.117 -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var nodemon = require('gulp-nodemon'); 3 | var gulpMocha = require('gulp-mocha'); 4 | var eslint = require('gulp-eslint'); 5 | var env = require('gulp-env'); 6 | 7 | gulp.task('default',['lint','test'], function(){ 8 | nodemon({ 9 | exec: 'node', 10 | script: 'server.js', 11 | ext: 'js', 12 | env: { 13 | PORT: 8080 14 | }, 15 | ignore: ['./node_modules/'] 16 | }) 17 | .on('restart',['test'], function(){ 18 | console.log('We have restarted'); 19 | }) 20 | }); 21 | 22 | gulp.task('test', function(){ 23 | env({vars:{ENV:'Test'}}); 24 | gulp.src(['Tests/singleFunctionTest.js','Tests/cityUpdateTest.js','Tests/activeUpdateTest.js'/*,'Tests/unitUpdateTest.js'*/]) 25 | .pipe(gulpMocha({reporter: 'nyan'})); 26 | }); 27 | 28 | gulp.task('lint', function () { 29 | return gulp.src(['**/*.js','!node_modules/**','!code graveyard/**','!Tests/**']) 30 | .pipe(eslint()) 31 | .pipe(eslint.format()); 32 | }); 33 | 34 | -------------------------------------------------------------------------------- /img/brand/expedia.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voipnorm/CiscoTPCustomXML/f603f3b40f6093c01faaaaf85f143382e2323615/img/brand/expedia.jpg -------------------------------------------------------------------------------- /img/oldImages/expedia.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voipnorm/CiscoTPCustomXML/f603f3b40f6093c01faaaaf85f143382e2323615/img/oldImages/expedia.jpg -------------------------------------------------------------------------------- /img/oldImages/kaiser-permanente272.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voipnorm/CiscoTPCustomXML/f603f3b40f6093c01faaaaf85f143382e2323615/img/oldImages/kaiser-permanente272.jpg -------------------------------------------------------------------------------- /img/wallpaper/Kaiser-Permanente-Exterior.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voipnorm/CiscoTPCustomXML/f603f3b40f6093c01faaaaf85f143382e2323615/img/wallpaper/Kaiser-Permanente-Exterior.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "branding", 3 | "version": "1.0.0", 4 | "description": "TP xml deployment app", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "bottleneck": "^2.2.2", 14 | "dotenv": "^5.0.1", 15 | "exceljs": "^0.9.1", 16 | "jsxapi": "^4.1.2", 17 | "lodash": "^4.17.5", 18 | "request": "^2.85.0", 19 | "xmlhttprequest": "^1.8.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | //Application Entry. Command line inut processed by switch command and launches the right function called. 2 | 3 | const log = require('./svrConfig/logger'); 4 | const comms = require('./svrConfig/comFunct'); 5 | 6 | 7 | 8 | switch (process.argv[2]) { 9 | case null: 10 | log.info("Command incomplete"); 11 | return log.info("Please specify your operation. Node command incomplete. Refer to readme for more instructions."); 12 | case "bundle": 13 | log.info("Deploying bundle"); 14 | return comms.bundle(); 15 | case 'branding': 16 | log.info("Deploy Branding"); 17 | return comms.branding(); 18 | case 'wallpaper': 19 | log.info("Deploy Wall paper"); 20 | return comms.wallpaper(); 21 | case 'logs': 22 | log.info("Deploy log collection."); 23 | return comms.logCollection(process.argv[3]); 24 | default: 25 | log.info("Command incomplete"); 26 | return log.info("Please specify your operation. Node command incomplete. Refer to readme for more instructions."); 27 | } 28 | -------------------------------------------------------------------------------- /svrConfig/comFunct.js: -------------------------------------------------------------------------------- 1 | const buildXml = require('../tools/buildXml'); 2 | const excel = require('../tools/excel'); 3 | const image64 = require('../tools/image64'); 4 | const filewatcher = require('../tools/filewatcher'); 5 | const _ = require('lodash'); 6 | const log = require('./logger'); 7 | const Endpoint = require('../tools/endpoint'); 8 | const ip = require('../tools/ipaddress'); 9 | const collect = require('../tools/collectLogs'); 10 | 11 | 12 | var brandingPath = './img/brand/'; 13 | var wallPaperPath = './img/wallpaper/'; 14 | 15 | 16 | var port = process.env.PORT || "9000"; 17 | var filePath = []; 18 | var deployEndpoints = []; 19 | var endpointArray = []; 20 | var backUpObj ={}; 21 | 22 | module.exports = { 23 | bundle: function(){ 24 | /*STEP 1. build http of host URL and create checksum of backup zip file 25 | STEP 2. Convert CSV of endpoints into array 26 | STEP 3. Build XML payload and deliver payload to each endpoint 27 | */ 28 | //URL consists of directory structure and IP address of machine deployed 29 | ip.getIPAddress() 30 | .then(ipString => { 31 | backUpObj.ip = ipString; 32 | return filewatcher.fileWatcherBackupBundle() 33 | }) 34 | .then(fileObj => { 35 | backUpObj.cs = fileObj.checksum; 36 | backUpObj.dir = fileObj.fileDir; 37 | return excel.readcsv() 38 | }) 39 | .then((endpoints) => { 40 | log.info("Processing branding xml to create new xml file......"); 41 | endpointArray = endpoints; 42 | var url = `http://${backUpObj.ip}:${port}/${backUpObj.dir}`; 43 | log.info(url) 44 | return buildXml.bundleXml(backUpObj.cs,url) 45 | }) 46 | .then((xmlReturn) => { 47 | log.info("XML deployment starting........ "); 48 | _.forEach(endpointArray, function(ip){ 49 | if(!ip) return log.info("Blank endpoint, no files deployed."); 50 | deployEndpoints.push(new Endpoint(ip, xmlReturn, "bundle")); 51 | }) 52 | }) 53 | .catch(err => { 54 | log.error(err) 55 | }); 56 | const httpServer = require('./httpServer'); 57 | 58 | }, 59 | branding: function(){ 60 | /*STEP 1. build base 64 string of image and create file location strings strings 61 | STEP 2. Convert CSV of endpoints into array 62 | STEP 3. Build XML payload and deliver payload to each endpoint 63 | */ 64 | log.info("Branding to be deployed."); 65 | Promise.resolve() 66 | .then(() => { 67 | return filewatcher.fileWatcher(); 68 | }) 69 | .then((files) => { 70 | log.info("Encoding images to base64 for deployment.... "); 71 | return image64.base64encode(files); 72 | }) 73 | .then((fileString) => { 74 | //log.info(fileString[1]); 75 | filePath = fileString; 76 | return excel.readcsv() 77 | }) 78 | .then((endpoints) => { 79 | log.info("Processing branding xml to create new xml file......"); 80 | endpointArray = endpoints; 81 | return buildXml.brandingXml(filePath); 82 | }) 83 | .then((xmlReturn) => { 84 | log.info("XML deployment starting........ "); 85 | _.forEach(endpointArray, function(ip){ 86 | if(!ip) return log.info("Blank endpoint, no files deployed."); 87 | deployEndpoints.push(new Endpoint(ip, xmlReturn, 'branding')); 88 | 89 | 90 | 91 | }) 92 | }) 93 | .catch(err => { 94 | log.error(err); 95 | }) 96 | 97 | }, 98 | logCollection: function(ip){ 99 | collect.collectLogs(ip) 100 | .then((response) => { 101 | log.info(response); 102 | }) 103 | .catch((err) => { 104 | log.error(err); 105 | }) 106 | }, 107 | wallpaper: function(){ 108 | const dir = "./img/wallpaper/"; 109 | if(!dir) return log.error("No file found, please ensure that your wallpaper file to be deployed is in /img/wallpaper."); 110 | return excel.readcsv() 111 | .then((endpoints) => { 112 | log.info("Processing branding xml to create new xml file......"); 113 | endpointArray = endpoints; 114 | }) 115 | .then(() => { 116 | log.info("XML deployment starting........ "); 117 | _.forEach(endpointArray, function(ip){ 118 | if(!ip) return log.info("Blank endpoint, no files deployed."); 119 | deployEndpoints.push(new Endpoint(ip, null, "wallpaper")); 120 | }) 121 | }) 122 | .catch(err => { 123 | log.error(err) 124 | }); 125 | } 126 | 127 | }; -------------------------------------------------------------------------------- /svrConfig/httpServer.js: -------------------------------------------------------------------------------- 1 | //application/zip, application/x-zip-compressed 2 | 3 | const http = require('http'); 4 | const url = require('url'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const log = require('./logger'); 8 | // you can pass the parameter in the command line. e.g. node static_server.js 3000 9 | const port = process.env.PORT || 9000; 10 | http.createServer(function (req, res) { 11 | log.info(`${req.method} ${req.url}`); 12 | // parse URL 13 | const parsedUrl = url.parse(req.url); 14 | // extract URL path 15 | let pathname = `.${parsedUrl.pathname}`; 16 | // maps file extention to MIME types 17 | const mimeType = { 18 | '.zip': 'application/zip' 19 | }; 20 | fs.exists(pathname, function (exist) { 21 | if(!exist) { 22 | // if the file is not found, return 404 23 | res.statusCode = 404; 24 | res.end(`File ${pathname} not found!`); 25 | return; 26 | } 27 | // if is a directory, then look for index.html 28 | if (fs.statSync(pathname).isDirectory()) { 29 | pathname += '/xmlFiles/backupBundle/backup-bundle.zip'; 30 | } 31 | // read file from file system 32 | fs.readFile(pathname, function(err, data){ 33 | if(err){ 34 | res.statusCode = 500; 35 | res.end(`Error getting the file: ${err}.`); 36 | } else { 37 | // based on the URL path, extract the file extention. e.g. .js, .doc, ... 38 | const ext = path.parse(pathname).ext; 39 | // if the file is found, set Content-type and send data 40 | res.setHeader('Content-type', mimeType[ext] || 'text/plain' ); 41 | res.end(data); 42 | } 43 | }); 44 | }); 45 | }).listen(parseInt(port)); 46 | log.info(`Server listening on port ${port}`); -------------------------------------------------------------------------------- /svrConfig/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const winston = require('winston'); 4 | const env = process.env.NODE_ENV; 5 | const {createLogger, format, transports} = require('winston'); 6 | const {combine, timestamp, label, printf, colorize} = format; 7 | 8 | 9 | const myFormat = printf(info => { 10 | return `${info.timestamp} ${info.level}: ${info.message}`; 11 | }); 12 | 13 | 14 | const logger = 15 | winston.createLogger({ 16 | format: combine( 17 | format.splat(), 18 | colorize({ all: true }), 19 | timestamp(), 20 | myFormat 21 | ), 22 | transports: [ 23 | // 24 | // - Write to all logs with level `info` and below to `combined.log` 25 | // - Write all logs error (and below) to `error.log`. 26 | // 27 | new winston.transports.File({ filename: './logs/error.log', level: 'error' }), 28 | new winston.transports.File({ filename: './logs/combined.log' }) 29 | ], 30 | exceptionHandlers: [ 31 | new winston.transports.File( { 32 | filename: 'logs/exceptions.log' 33 | } ), 34 | new winston.transports.Console( { 35 | colorize: true 36 | } ), 37 | ] 38 | }); 39 | 40 | // 41 | // If we're not in production then log to the `console` with the format: 42 | // `${info.level}: ${info.message} JSON.stringify({ ...rest }) ` 43 | // 44 | if (env !== 'production') { 45 | logger.add(new winston.transports.Console()); 46 | } 47 | 48 | //logger.info('Hello, this is a logging event with a custom pretty print', { 'foo': 'bar' }); 49 | //logger.info('Hello, this is a logging event with a custom pretty print2', { 'foo': 'bar' }); 50 | 51 | module.exports = logger; 52 | 53 | -------------------------------------------------------------------------------- /tools/buildXml.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | brandingXml: function(base64){ 4 | return new Promise(function(resolve){ 5 | var base64Brand = base64[0]; 6 | var base64wp = base64[1]; 7 | var xml2 = ` 8 | 9 | 10 | 11 | HalfwakeBranding 12 | ${base64Brand} 13 | 14 | 15 | Branding 16 | ${base64Brand} 17 | 18 | 19 | HalfwakeBackground 20 | ${base64wp} 21 | 22 | 23 | 24 | `; 25 | 26 | resolve(xml2) 27 | 28 | }) 29 | 30 | }, 31 | inRoomXml: function(){ 32 | return new Promise(function(resolve) { 33 | 34 | }) 35 | }, 36 | macroXml: function(){ 37 | return new Promise(function(resolve){ 38 | 39 | }) 40 | 41 | }, 42 | bundleXml: function(checksum, url){ 43 | return new Promise(function(resolve){ 44 | var xml2 = ` 45 | 46 | 47 | 48 | ${url} 49 | ${checksum} 50 | 51 | 52 | 53 | `; 54 | resolve(xml2) 55 | }) 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /tools/collectLogs.js: -------------------------------------------------------------------------------- 1 | //download logs from a endpoint 2 | 3 | require('dotenv').config(); 4 | const request = require('request'); 5 | const fs = require('fs'); 6 | const username = process.env.TPADMIN || "admin"; 7 | const password = process.env.TPADMINPWD || "password"; 8 | 9 | module.exports = { 10 | collectLogs : function(ip) { 11 | return new Promise(function (resolve) { 12 | const url = `http://${ip}/api/logs/download`; 13 | const r = request.get(url).auth(username, password, false); 14 | r.on('response', function (res) { 15 | res.pipe(fs.createWriteStream('./endpointLogs/' + res.headers.date + '.' + res.headers['content-type'].split('/')[1])) 16 | res.on('end', function() { resolve("write complete") }); 17 | }) 18 | }) 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /tools/endpoint.js: -------------------------------------------------------------------------------- 1 | //creates main endpoint object for TP endpoint 2 | /* 3 | multipart/form-data 4 | */ 5 | 6 | require('dotenv').config(); 7 | const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest; 8 | 9 | const util = require('util'); 10 | const EventEmitter = require('events').EventEmitter; 11 | const TpXapi = require('./tpXapi'); 12 | const log = require('../svrConfig/logger'); 13 | const request = require('request'); 14 | const fs = require('fs'); 15 | 16 | //pass in object versus single values 17 | function Endpoint(ip,xml,type){ 18 | this.ipAddress = ip; 19 | this.password = process.env.TPADMINPWD; 20 | this.username = process.env.TPADMIN; 21 | this.url = `http://${ip}/putxml`; 22 | this.wallpaperUrl = `http://${ip}/api/wallpapers`; 23 | this.version = ''; 24 | this.xml = xml; 25 | this.type = type; 26 | this.init(); 27 | } 28 | 29 | util.inherits(Endpoint,EventEmitter); 30 | 31 | Endpoint.prototype.init = function(){ 32 | var self = this; 33 | //insert version checker to work out best thing to deploy. Should check version for back-bundles as well. 34 | self.firmwareCheck() 35 | .then((data) => { 36 | var version = data.version.slice(2,7); 37 | var tpType = data.type; 38 | log.info(version + tpType); 39 | if(self.type==="branding") { 40 | if (tpType === "SX10") { 41 | return self.postWallpaper(); 42 | } else { 43 | switch (true) { 44 | case (/(^)9.3( |.|$)/).test(version): 45 | return self.deployXml(); 46 | case (/(^)9.2( |.|$)/).test(version): 47 | return self.deployXml(); 48 | case (/(^)9.1( |.|$)/).test(version): 49 | return self.postWallpaper(); 50 | case (/(^)7( |.|$)/).test(version): 51 | return self.postWallpaper(); 52 | default: 53 | return log.info("Something went wrong with firmware check"); 54 | } 55 | } 56 | }else if(self.type==="wallpaper"){ 57 | return self.postWallpaper(); 58 | }else{ 59 | switch (true) { 60 | case (/(^)9.3( |.|$)/).test(version): 61 | return self.deployXml(); 62 | default: 63 | return log.info("Endpoint not compatible with bundling feature. Must be CE9.3 or later: " +self.ipAdress+' : '+self.version); 64 | } 65 | } 66 | 67 | }) 68 | .catch(err => { 69 | log.error(err); 70 | }) 71 | 72 | }; 73 | //check what version of software is on the endpoint 74 | Endpoint.prototype.firmwareCheck = function (){ 75 | var self = this; 76 | return new Promise(function(resolve){ 77 | log.info("firmwareCheck launched ..."); 78 | 79 | var ep = { 80 | "password": self.password, 81 | "username": self.username, 82 | "ipAddress" : self.ipAddress 83 | }; 84 | var videoCodec = new TpXapi(ep); 85 | videoCodec.getEndpointData() 86 | .then((endpoint) => { 87 | return resolve(endpoint); 88 | }) 89 | .catch(err => { 90 | log.error(err); 91 | }); 92 | 93 | 94 | }) 95 | 96 | }; 97 | 98 | Endpoint.prototype.deployXml = function(){ 99 | var self = this; 100 | var mimeType = "text/xml"; 101 | const xmlHttp = new XMLHttpRequest(); 102 | xmlHttp.onreadystatechange = function() { 103 | 104 | if (xmlHttp.readyState === 4) { 105 | log.info("State: " + this.readyState); 106 | 107 | 108 | if (this.readyState === 4) { 109 | log.info(null,"Complete.\nBody length: " + this.responseText.length); 110 | log.info("Body:\n" + this.responseText+xmlHttp.DONE); 111 | if(xmlHttp.DONE === 4 && this.responseText.length > 1) return log.info("Package Deployed to "+self.ipAddress); 112 | } 113 | 114 | } 115 | } 116 | xmlHttp.open('POST', self.url, true, self.username, self.password); 117 | xmlHttp.setRequestHeader('Content-Type', mimeType); 118 | xmlHttp.withCredentials = true; 119 | xmlHttp.send(this.xml); 120 | }; 121 | 122 | Endpoint.prototype.postWallpaper = function(){ 123 | log.info("Posting wall paper"); 124 | const self = this; 125 | const dir = "./img/wallpaper/"; 126 | var fileName = fs.readdirSync('./img/wallpaper/'); 127 | if(!dir) log.error("No file found"); 128 | var fileString = dir+fileName; 129 | log.info(fileString); 130 | var formData = { 131 | 132 | file: { 133 | value: fs.createReadStream(fileString), 134 | options: { 135 | filename: fileName[0], 136 | contentType: 'image/jpeg' 137 | } 138 | } 139 | }; 140 | log.info(JSON.stringify(formData)); 141 | var r = request.post({url:`http://${self.ipAddress}/api/wallpapers`, formData: formData},function optionalCallback(err, httpResponse, body) { 142 | if (err) { 143 | return console.error('upload failed:', err); 144 | } 145 | console.log('Upload successful! Server responded with:', body); 146 | }).auth(self.username, self.password, false); 147 | 148 | 149 | 150 | }; 151 | 152 | 153 | module.exports = Endpoint; 154 | -------------------------------------------------------------------------------- /tools/excel.js: -------------------------------------------------------------------------------- 1 | //module for reading CSV file downloaded from Spark for uploading bulk TP endpoints - needs work on adding validy of CSV format 2 | 3 | var Excel = require('exceljs'); 4 | var fs = require('fs'); 5 | var workbook = new Excel.Workbook(); 6 | var log = require('../svrConfig/logger'); 7 | 8 | 9 | 10 | 11 | module.exports = { 12 | readcsv: function(){ 13 | return new Promise(function(resolve, reject){ 14 | var endpoints = []; 15 | var filename; 16 | var fileDir = './endpoints/'; 17 | var file = fs.readdirSync(fileDir); 18 | log.info(file); 19 | filename = fileDir+file; 20 | 21 | log.info("Reading CSV, creating endpoint array for deployment...") 22 | 23 | workbook.csv.readFile(filename) 24 | .then(function(worksheet) { 25 | 26 | for(var i = 0; i= 1) { 17 | // this single interface has multiple ipv4 addresses 18 | resolve(iface.address); 19 | } else { 20 | // this interface has only one ipv4 adress 21 | resolve(iface.address); 22 | } 23 | ++alias; 24 | }); 25 | }); 26 | }) 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /tools/tpXapi.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const EventEmitter = require('events').EventEmitter; 3 | const log = require('../svrConfig/logger'); 4 | const jsxapi = require('jsxapi'); 5 | 6 | //pass in object versus single values 7 | function TPXapi(endpoint){ 8 | this.endpoint = endpoint; 9 | this.xapi; 10 | this.endpointVersion; 11 | this.endpointType; 12 | 13 | }; 14 | 15 | util.inherits(TPXapi,EventEmitter); 16 | 17 | //force update of data from endpoint 18 | TPXapi.prototype.getEndpointData = function(){ 19 | return new Promise((resolve, reject) => { 20 | const self = this; 21 | return self.endpointUpdate() 22 | .then(() => { 23 | let endpoint = { 24 | version : self.endpointVersion, 25 | type : self.endpointType 26 | } 27 | return resolve(endpoint); 28 | }) 29 | .catch(err => { 30 | log.error(err) 31 | }) 32 | }); 33 | }; 34 | 35 | 36 | TPXapi.prototype.endpointUpdate = function(){ 37 | const self = this; 38 | return self.connect() 39 | .then((version) => { 40 | log.info(version); 41 | return self.checkVersion(); 42 | }) 43 | .then((status) => { 44 | log.info(status); 45 | return self.checkType() 46 | }) 47 | .then((type) => { 48 | log.info("The type is: "+type); 49 | return self.closeConnect() 50 | }) 51 | .catch((err) => { 52 | log.error(err); 53 | }) 54 | } 55 | 56 | //connect to ssh service on endpoints 57 | TPXapi.prototype.connect = function() { 58 | var self = this; 59 | log.info(JSON.stringify(self.endpoint)); 60 | return new Promise((resolve, reject) => { 61 | self.xapi = jsxapi.connect('ssh://' + self.endpoint.ipAddress, { 62 | username: self.endpoint.username, 63 | password: self.endpoint.password 64 | }); 65 | resolve ("Connection open") 66 | .catch ((err) => { 67 | reject (log.error(err)); 68 | }); 69 | }); 70 | } 71 | 72 | TPXapi.prototype.checkVersion = function(){ 73 | //SystemUnit Software Version 74 | const self = this; 75 | return new Promise((resolve, reject) => { 76 | return self.xapi.status 77 | .get('SystemUnit Software Version') 78 | .then((version) => { 79 | self.endpointVersion = version; 80 | resolve(version); 81 | }) 82 | .catch(err => reject(err)); 83 | }) 84 | }; 85 | TPXapi.prototype.checkType = function(){ 86 | //SystemUnit type 87 | log.info("info: Checking system type.") 88 | const self = this; 89 | return new Promise((resolve, reject) => { 90 | return self.xapi.status 91 | .get('SystemUnit ProductPlatform') 92 | .then((type) => { 93 | self.endpointType = type; 94 | resolve(type); 95 | }) 96 | .catch(err => reject(err)); 97 | }) 98 | }; 99 | 100 | 101 | //close ssh connection 102 | TPXapi.prototype.closeConnect = function(){ 103 | const self = this; 104 | return new Promise((resolve, reject) => { 105 | log.info("xapi session closed."); 106 | self.connectedStatus = "false"; 107 | resolve (self.xapi.close()); 108 | 109 | return self; 110 | 111 | }) 112 | }; 113 | 114 | 115 | module.exports = TPXapi; -------------------------------------------------------------------------------- /xmlFiles/backupBundle/backup-bundle.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voipnorm/CiscoTPCustomXML/f603f3b40f6093c01faaaaf85f143382e2323615/xmlFiles/backupBundle/backup-bundle.zip -------------------------------------------------------------------------------- /xmlFiles/macros/proximityMacro.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Macro companion to the Ultrasound Control 3 | * - lets users toggle Proximity Mode to On/Off 4 | * - displays the current MaxVolume level 5 | */ 6 | 7 | const xapi = require('xapi'); 8 | const proximityID = 'ProximityOnOff'; 9 | const textBoxId = 'textBoxProximity'; 10 | 11 | // Change proximity mode to "On" or "Off" 12 | function switchProximityMode(mode) { 13 | console.debug(`switching proximity mode to: ${mode}`); 14 | 15 | xapi.config.set('Proximity Mode', mode) 16 | .then(() => { 17 | console.info(`turned proximity mode: ${mode}`) 18 | }) 19 | .catch((err) => { 20 | console.error(`could not turn proximity mode: ${mode}`) 21 | }) 22 | } 23 | 24 | // React to UI events 25 | function onGui(event) { 26 | // Proximity Mode Switch 27 | if ((event.Type === 'changed') && (event.WidgetId === proximityID||event.WidgetId === "proximity_toggle")) { 28 | switchProximityMode(event.Value) 29 | return; 30 | } 31 | } 32 | xapi.event.on('UserInterface Extensions Widget Action', onGui); 33 | 34 | 35 | // 36 | // Proximity Services Availability 37 | // 38 | 39 | // Update Toogle if proximity mode changes 40 | function updateProximityToggle(mode) { 41 | console.debug(`switching toggle to ${mode}`) 42 | 43 | xapi.command("UserInterface Extensions Widget SetValue", { 44 | WidgetId: proximityID, 45 | Value: mode 46 | }) 47 | xapi.command("UserInterface Extensions Widget SetValue", { 48 | WidgetId: "proximity_toggle", 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 | textBoxUpdate(mode); 59 | }) 60 | 61 | // Refresh Toggle state 62 | function refreshProximityToggle() { 63 | xapi.status.get("Proximity Services Availability") 64 | .then(availability => { 65 | console.debug(`current proximity mode is ${availability}`) 66 | switch (availability) { 67 | case 'Available': 68 | updateProximityToggle('on'); 69 | textBoxUpdate('On'); 70 | return; 71 | 72 | case 'Disabled': 73 | default: 74 | updateProximityToggle('off') 75 | textBoxUpdate('Off') 76 | return; 77 | } 78 | }) 79 | .catch((err) => { 80 | console.error(`could not read current proximity mode, err: ${err.message}`) 81 | }) 82 | } 83 | //update text box to show proximity status 84 | function textBoxUpdate(stringValue){ 85 | xapi.command('UserInterface Extensions Widget SetValue', { 86 | WidgetId: textBoxId, 87 | Value: "Proximity "+stringValue, 88 | }); 89 | } 90 | // Initialize at widget deployment 91 | xapi.event.on('UserInterface Extensions Widget LayoutUpdated', (event) => { 92 | console.debug("layout updated, let's refresh our toogle"); 93 | refreshProximityToggle() 94 | }); -------------------------------------------------------------------------------- /xmlFiles/readme.txt: -------------------------------------------------------------------------------- 1 | Use this folder to stash backup-bundle, roomControl xml and Macro files that need to be deployed to your endpoints. 2 | The application will suck up these files into the correct XML format to deploy to your endpoints. 3 | 4 | Backup bundles file currently in file is example only, make sure to delete and replace with your own. 5 | 6 | In the case of the backup-bdunle deploymnet in CE9.3 a checksum will be created and allplication provides a 7 | HTTP server to allow endpoints to download bundle package. 8 | 9 | The proximity files are here as an working example. If you do not want this deployed to your endpoints make sure to remove before deployment. -------------------------------------------------------------------------------- /xmlFiles/roomControls/proximityButton.xml: -------------------------------------------------------------------------------- 1 |  2 | 1.5 3 | 4 | Proximity 5 | Statusbar 6 | Proximity 7 | 1 8 | #A866FF 9 | Proximity 10 | 11 | Page 12 | 13 | Proximity 14 | 15 | proximity_toggle 16 | ToggleButton 17 | size=1 18 | 19 | 20 | 21 | 22 | 23 | 24 | --------------------------------------------------------------------------------