script.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. var FORM_SETTINGS = [
  2. "admin_username", "admin_password", "ce_pin", "csn_pin", "reset_pin","packet_repeats",
  3. "http_repeat_factor", "auto_restart_period", "discovery_port", "mqtt_server",
  4. "mqtt_topic_pattern", "mqtt_update_topic_pattern", "mqtt_username", "mqtt_password",
  5. "radio_interface_type", "listen_repeats"
  6. ];
  7. var FORM_SETTINGS_HELP = {
  8. ce_pin : "'CE' for NRF24L01 interface, and 'PKT' for 'PL1167/LT8900' interface",
  9. packet_repeats : "The number of times to repeat RF packets sent to bulbs",
  10. http_repeat_factor : "Multiplicative factor on packet_repeats for " +
  11. "requests initiated by the HTTP API. UDP API typically receives " +
  12. "duplicate packets, so more repeats should be used for HTTP.",
  13. auto_restart_period : "Automatically restart the device every number of " +
  14. "minutes specified. Use 0 for disabled.",
  15. radio_interface_type : "2.4 GHz radio model. Only change this if you know " +
  16. "You're not using an NRF24L01!",
  17. mqtt_server : "Domain or IP address of MQTT broker. Optionally specify a port " +
  18. "with (example) mymqqtbroker.com:1884.",
  19. mqtt_topic_pattern : "Pattern for MQTT topics to listen on. Example: " +
  20. "lights/:device_id/:device_type/:group_id. See README for further details.",
  21. mqtt_update_topic_pattern : "Pattern to publish MQTT updates. Packets that " +
  22. "are received from other devices, and packets that are sent from this device will " +
  23. "result in updates being sent.",
  24. discovery_port : "UDP port to listen for discovery packets on. Defaults to " +
  25. "the same port used by MiLight devices, 48899. Use 0 to disable.",
  26. listen_repeats : "Increasing this increases the amount of time spent listening for " +
  27. "packets. Set to 0 to disable listening. Default is 3."
  28. }
  29. var UDP_PROTOCOL_VERSIONS = [ 5, 6 ];
  30. var DEFAULT_UDP_PROTOCL_VERSION = 5;
  31. var selectize;
  32. var toHex = function(v) {
  33. return "0x" + (v).toString(16).toUpperCase();
  34. }
  35. var activeUrl = function() {
  36. var deviceId = $('#deviceId option:selected').val()
  37. , groupId = $('#groupId .active input').data('value')
  38. , mode = getCurrentMode();
  39. if (deviceId == "") {
  40. alert("Please enter a device ID.");
  41. throw "Must enter device ID";
  42. }
  43. if (! $('#group-option').data('for').split(',').includes(mode)) {
  44. groupId = 0;
  45. }
  46. return "/gateways/" + deviceId + "/" + mode + "/" + groupId;
  47. }
  48. var getCurrentMode = function() {
  49. return $('input[name="mode"]:checked').data('value');
  50. };
  51. var updateGroup = _.throttle(
  52. function(params) {
  53. $.ajax(
  54. activeUrl(),
  55. {
  56. method: 'PUT',
  57. data: JSON.stringify(params),
  58. contentType: 'application/json'
  59. }
  60. );
  61. },
  62. 1000
  63. );
  64. var sendCommand = _.throttle(
  65. function(params) {
  66. $.ajax(
  67. '/system',
  68. {
  69. method: 'POST',
  70. data: JSON.stringify(params),
  71. contentType: 'application/json'
  72. }
  73. );
  74. },
  75. 1000
  76. )
  77. var sniffRequest;
  78. var sniffing = false;
  79. var getTraffic = function() {
  80. sniffRequest = $.get('/gateway_traffic', function(data) {
  81. $('#sniffed-traffic').prepend('<pre>' + data + '</pre>');
  82. getTraffic();
  83. });
  84. };
  85. var gatewayServerRow = function(deviceId, port, version) {
  86. var elmt = '<tr>';
  87. elmt += '<td>';
  88. elmt += '<input name="deviceIds[]" class="form-control" value="' + deviceId + '"/>';
  89. elmt += '</td>';
  90. elmt += '<td>'
  91. elmt += '<input name="ports[]" class="form-control" value="' + port + '"/>';;
  92. elmt += '</td>';
  93. elmt += '<td>';
  94. elmt += '<div class="btn-group" data-toggle="buttons">';
  95. for (var i = 0; i < UDP_PROTOCOL_VERSIONS.length; i++) {
  96. var val = UDP_PROTOCOL_VERSIONS[i]
  97. , selected = (version == val || (val == DEFAULT_UDP_PROTOCL_VERSION && !UDP_PROTOCOL_VERSIONS.includes(version)));
  98. elmt += '<label class="btn btn-secondary' + (selected ? ' active' : '') + '">';
  99. elmt += '<input type="radio" name="versions[]" autocomplete="off" data-value="' + val + '" '
  100. + (selected ? 'checked' : '') +'> ' + val;
  101. elmt += '</label>';
  102. }
  103. elmt += '</div></td>';
  104. elmt += '<td>';
  105. elmt += '<button class="btn btn-danger remove-gateway-server">';
  106. elmt += '<i class="glyphicon glyphicon-remove"></i>';
  107. elmt += '</button>';
  108. elmt += '</td>';
  109. elmt += '</tr>';
  110. return elmt;
  111. }
  112. var loadSettings = function() {
  113. $.getJSON('/settings', function(val) {
  114. Object.keys(val).forEach(function(k) {
  115. var field = $('#settings input[name="' + k + '"]');
  116. if (field.length > 0) {
  117. if (field.attr('type') === 'radio') {
  118. field.filter('[value="' + val[k] + '"]').click();
  119. } else {
  120. field.val(val[k]);
  121. }
  122. }
  123. });
  124. if (val.device_ids) {
  125. selectize.clearOptions();
  126. val.device_ids.forEach(function(v) {
  127. selectize.addOption({text: toHex(v), value: v});
  128. });
  129. selectize.refreshOptions();
  130. }
  131. var gatewayForm = $('#gateway-server-configs').html('');
  132. if (val.gateway_configs) {
  133. val.gateway_configs.forEach(function(v) {
  134. gatewayForm.append(gatewayServerRow(toHex(v[0]), v[1], v[2]));
  135. });
  136. }
  137. });
  138. };
  139. var saveGatewayConfigs = function() {
  140. var form = $('#gateway-server-form')
  141. , errors = false;
  142. $('input', form).removeClass('error');
  143. var deviceIds = $('input[name="deviceIds[]"]', form).map(function(i, v) {
  144. var val = $(v).val();
  145. if (isNaN(val)) {
  146. errors = true;
  147. $(v).addClass('error');
  148. return null;
  149. } else {
  150. return val;
  151. }
  152. });
  153. var ports = $('input[name="ports[]"]', form).map(function(i, v) {
  154. var val = $(v).val();
  155. if (isNaN(val)) {
  156. errors = true;
  157. $(v).addClass('error');
  158. return null;
  159. } else {
  160. return val;
  161. }
  162. });
  163. var versions = $('.active input[name="versions[]"]', form).map(function(i, v) {
  164. return $(v).data('value');
  165. });
  166. if (!errors) {
  167. var data = [];
  168. for (var i = 0; i < deviceIds.length; i++) {
  169. data[i] = [deviceIds[i], ports[i], versions[i]];
  170. }
  171. $.ajax(
  172. '/settings',
  173. {
  174. method: 'put',
  175. contentType: 'application/json',
  176. data: JSON.stringify({gateway_configs: data})
  177. }
  178. )
  179. }
  180. };
  181. var deviceIdError = function(v) {
  182. if (!v) {
  183. $('#device-id-label').removeClass('error');
  184. } else {
  185. $('#device-id-label').addClass('error');
  186. $('#device-id-label .error-info').html(v);
  187. }
  188. };
  189. var updateModeOptions = function() {
  190. var currentMode = getCurrentMode();
  191. $('.mode-option').map(function() {
  192. if ($(this).data('for').split(',').includes(currentMode)) {
  193. $(this).show();
  194. } else {
  195. $(this).hide();
  196. }
  197. });
  198. };
  199. var parseVersion = function(v) {
  200. var matches = v.match(/(\d+)\.(\d+)\.(\d+)(-(.*))?/);
  201. return {
  202. major: matches[1],
  203. minor: matches[2],
  204. patch: matches[3],
  205. revision: matches[5],
  206. parts: [matches[1], matches[2], matches[3], matches[5]]
  207. };
  208. };
  209. var isNewerVersion = function(a, b) {
  210. var va = parseVersion(a)
  211. , vb = parseVersion(b);
  212. return va.parts > vb.parts;
  213. };
  214. var handleCheckForUpdates = function() {
  215. var currentVersion = null
  216. , latestRelease = null;
  217. var handleReceiveData = function() {
  218. if (currentVersion != null) {
  219. $('#current-version').html(currentVersion.version + " (" + currentVersion.variant + ")");
  220. }
  221. if (latestRelease != null) {
  222. $('#latest-version .info-key').each(function() {
  223. var value = latestRelease[$(this).data('key')];
  224. var prop = $(this).data('prop');
  225. if (prop) {
  226. $(this).prop(prop, value);
  227. } else {
  228. $(this).html(value);
  229. }
  230. });
  231. }
  232. if (currentVersion != null && latestRelease != null) {
  233. $('#latest-version .status').html('');
  234. $('#latest-version-info').show();
  235. var summary;
  236. if (isNewerVersion(latestRelease.tag_name, currentVersion.version)) {
  237. summary = "New version available!";
  238. } else {
  239. summary = "You're on the most recent version.";
  240. }
  241. $('#version-summary').html(summary);
  242. var releaseAsset = latestRelease.assets.filter(function(x) {
  243. return x.name.indexOf(currentVersion.variant) != -1;
  244. });
  245. if (releaseAsset.length > 0) {
  246. console.log(releaseAsset[0].url);
  247. $('#firmware-link').prop('href', releaseAsset[0].browser_download_url);
  248. }
  249. }
  250. console.log(latestRelease);
  251. }
  252. var handleError = function(e, d) {
  253. console.log(e);
  254. console.log(d);
  255. }
  256. $('#current-version,#latest-version .status').html('<i class="spinning glyphicon glyphicon-refresh"></i>');
  257. $('#version-summary').html('');
  258. $('#latest-version-info').hide();
  259. $('#updates-modal').modal();
  260. $.ajax(
  261. '/about',
  262. {
  263. success: function(data) {
  264. currentVersion = JSON.parse(data);
  265. handleReceiveData();
  266. },
  267. failure: handleError
  268. }
  269. );
  270. $.ajax(
  271. '/latest_release',
  272. {
  273. success: function(data) {
  274. latestRelease = data;
  275. handleReceiveData();
  276. },
  277. failure: handleError
  278. }
  279. );
  280. };
  281. $(function() {
  282. $('.radio-option').click(function() {
  283. $(this).prev().prop('checked', true);
  284. });
  285. var hueDragging = false;
  286. var colorUpdated = function(e) {
  287. var x = e.pageX - $(this).offset().left
  288. , pct = x/(1.0*$(this).width())
  289. , hue = Math.round(360*pct)
  290. ;
  291. $('.hue-value-display').css({
  292. backgroundColor: "hsl(" + hue + ",100%,50%)"
  293. });
  294. updateGroup({hue: hue});
  295. };
  296. $('.hue-picker-inner')
  297. .mousedown(function(e) {
  298. hueDragging = true;
  299. colorUpdated.call(this, e);
  300. })
  301. .mouseup(function(e) {
  302. hueDragging = false;
  303. })
  304. .mouseout(function(e) {
  305. hueDragging = false;
  306. })
  307. .mousemove(function(e) {
  308. if (hueDragging) {
  309. colorUpdated.call(this, e);
  310. }
  311. });
  312. $('.slider').slider();
  313. $('.raw-update').change(function() {
  314. var data = {}
  315. , val = $(this).attr('type') == 'checkbox' ? $(this).is(':checked') : $(this).val()
  316. ;
  317. data[$(this).attr('name')] = val;
  318. updateGroup(data);
  319. });
  320. $('.command-btn').click(function() {
  321. updateGroup({command: $(this).data('command')});
  322. });
  323. $('.system-btn').click(function() {
  324. sendCommand({command: $(this).data('command')});
  325. });
  326. $('#sniff').click(function() {
  327. if (sniffing) {
  328. sniffRequest.abort();
  329. sniffing = false;
  330. $(this).html('Start Sniffing');
  331. } else {
  332. sniffing = true;
  333. getTraffic();
  334. $(this).html('Stop Sniffing');
  335. }
  336. });
  337. $('#add-server-btn').click(function() {
  338. $('#gateway-server-configs').append(gatewayServerRow('', ''));
  339. });
  340. $('#mode').change(updateModeOptions);
  341. $('body').on('click', '.remove-gateway-server', function() {
  342. $(this).closest('tr').remove();
  343. });
  344. selectize = $('#deviceId').selectize({
  345. create: true,
  346. sortField: 'text',
  347. onOptionAdd: function(v, item) {
  348. item.value = parseInt(item.value);
  349. },
  350. createFilter: function(v) {
  351. if (! v.match(/^(0x[a-fA-F0-9]{1,4}|[0-9]{1,5})$/)) {
  352. deviceIdError("Must be an integer between 0x0000 and 0xFFFF");
  353. return false;
  354. }
  355. var value = parseInt(v);
  356. if (! (0 <= v && v <= 0xFFFF)) {
  357. deviceIdError("Must be an integer between 0x0000 and 0xFFFF");
  358. return false;
  359. }
  360. deviceIdError(false);
  361. return true;
  362. }
  363. });
  364. selectize = selectize[0].selectize;
  365. var settings = "";
  366. FORM_SETTINGS.forEach(function(k) {
  367. var elmt = '<div class="form-entry">';
  368. elmt += '<div>';
  369. elmt += '<label for="' + k + '">' + k + '</label>';
  370. if (FORM_SETTINGS_HELP[k]) {
  371. elmt += '<div class="field-help" data-help-text="' + FORM_SETTINGS_HELP[k] + '"></div>';
  372. }
  373. elmt += '</div>';
  374. if(k === "radio_interface_type") {
  375. elmt += '<div class="btn-group" id="radio_interface_type" data-toggle="buttons">' +
  376. '<label class="btn btn-secondary active">' +
  377. '<input type="radio" id="nrf24" name="radio_interface_type" autocomplete="off" value="nRF24" /> nRF24' +
  378. '</label>'+
  379. '<label class="btn btn-secondary">' +
  380. '<input type="radio" id="lt8900" name="radio_interface_type" autocomplete="off" value="LT8900" /> PL1167/LT8900' +
  381. '</label>' +
  382. '</div>';
  383. } else {
  384. elmt += '<input type="text" class="form-control" name="' + k + '"/>';
  385. elmt += '</div>';
  386. }
  387. settings += elmt;
  388. });
  389. $('#settings').prepend(settings);
  390. $('#settings').submit(function(e) {
  391. var obj = {};
  392. FORM_SETTINGS.forEach(function(k) {
  393. var elmt = $('#settings input[name="' + k + '"]');
  394. if (elmt.attr('type') === 'radio') {
  395. obj[k] = elmt.filter(':checked').val();
  396. } else {
  397. obj[k] = elmt.val();
  398. }
  399. });
  400. // pretty hacky. whatever.
  401. obj.device_ids = _.map(
  402. $('.selectize-control .option'),
  403. function(x) {
  404. return $(x).data('value')
  405. }
  406. );
  407. $.ajax(
  408. "/settings",
  409. {
  410. method: 'put',
  411. contentType: 'application/json',
  412. data: JSON.stringify(obj)
  413. }
  414. );
  415. e.preventDefault();
  416. return false;
  417. });
  418. $('#gateway-server-form').submit(function(e) {
  419. saveGatewayConfigs();
  420. e.preventDefault();
  421. return false;
  422. });
  423. $('.field-help').each(function() {
  424. var elmt = $('<i></i>')
  425. .addClass('glyphicon glyphicon-question-sign')
  426. .tooltip({
  427. placement: 'top',
  428. title: $(this).data('help-text'),
  429. container: 'body'
  430. });
  431. $(this).append(elmt);
  432. });
  433. $('#updates-btn').click(handleCheckForUpdates);
  434. loadSettings();
  435. updateModeOptions();
  436. });