script.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  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. $('#firmware-link').prop('href', releaseAsset[0].browser_download_url);
  247. }
  248. }
  249. }
  250. var handleError = function(e, d) {
  251. console.log(e);
  252. console.log(d);
  253. }
  254. $('#current-version,#latest-version .status').html('<i class="spinning glyphicon glyphicon-refresh"></i>');
  255. $('#version-summary').html('');
  256. $('#latest-version-info').hide();
  257. $('#updates-modal').modal();
  258. $.ajax(
  259. '/about',
  260. {
  261. success: function(data) {
  262. currentVersion = data;
  263. handleReceiveData();
  264. },
  265. failure: handleError
  266. }
  267. );
  268. $.ajax(
  269. 'https://api.github.com/repos/sidoh/esp8266_milight_hub/releases/latest',
  270. {
  271. success: function(data) {
  272. latestRelease = data;
  273. handleReceiveData();
  274. },
  275. failure: handleError
  276. }
  277. );
  278. };
  279. $(function() {
  280. $('.radio-option').click(function() {
  281. $(this).prev().prop('checked', true);
  282. });
  283. var hueDragging = false;
  284. var colorUpdated = function(e) {
  285. var x = e.pageX - $(this).offset().left
  286. , pct = x/(1.0*$(this).width())
  287. , hue = Math.round(360*pct)
  288. ;
  289. $('.hue-value-display').css({
  290. backgroundColor: "hsl(" + hue + ",100%,50%)"
  291. });
  292. updateGroup({hue: hue});
  293. };
  294. $('.hue-picker-inner')
  295. .mousedown(function(e) {
  296. hueDragging = true;
  297. colorUpdated.call(this, e);
  298. })
  299. .mouseup(function(e) {
  300. hueDragging = false;
  301. })
  302. .mouseout(function(e) {
  303. hueDragging = false;
  304. })
  305. .mousemove(function(e) {
  306. if (hueDragging) {
  307. colorUpdated.call(this, e);
  308. }
  309. });
  310. $('.slider').slider();
  311. $('.raw-update').change(function() {
  312. var data = {}
  313. , val = $(this).attr('type') == 'checkbox' ? $(this).is(':checked') : $(this).val()
  314. ;
  315. data[$(this).attr('name')] = val;
  316. updateGroup(data);
  317. });
  318. $('.command-btn').click(function() {
  319. updateGroup({command: $(this).data('command')});
  320. });
  321. $('.system-btn').click(function() {
  322. sendCommand({command: $(this).data('command')});
  323. });
  324. $('#sniff').click(function() {
  325. if (sniffing) {
  326. sniffRequest.abort();
  327. sniffing = false;
  328. $(this).html('Start Sniffing');
  329. } else {
  330. sniffing = true;
  331. getTraffic();
  332. $(this).html('Stop Sniffing');
  333. }
  334. });
  335. $('#add-server-btn').click(function() {
  336. $('#gateway-server-configs').append(gatewayServerRow('', ''));
  337. });
  338. $('#mode').change(updateModeOptions);
  339. $('body').on('click', '.remove-gateway-server', function() {
  340. $(this).closest('tr').remove();
  341. });
  342. selectize = $('#deviceId').selectize({
  343. create: true,
  344. sortField: 'text',
  345. onOptionAdd: function(v, item) {
  346. item.value = parseInt(item.value);
  347. },
  348. createFilter: function(v) {
  349. if (! v.match(/^(0x[a-fA-F0-9]{1,4}|[0-9]{1,5})$/)) {
  350. deviceIdError("Must be an integer between 0x0000 and 0xFFFF");
  351. return false;
  352. }
  353. var value = parseInt(v);
  354. if (! (0 <= v && v <= 0xFFFF)) {
  355. deviceIdError("Must be an integer between 0x0000 and 0xFFFF");
  356. return false;
  357. }
  358. deviceIdError(false);
  359. return true;
  360. }
  361. });
  362. selectize = selectize[0].selectize;
  363. var settings = "";
  364. FORM_SETTINGS.forEach(function(k) {
  365. var elmt = '<div class="form-entry">';
  366. elmt += '<div>';
  367. elmt += '<label for="' + k + '">' + k + '</label>';
  368. if (FORM_SETTINGS_HELP[k]) {
  369. elmt += '<div class="field-help" data-help-text="' + FORM_SETTINGS_HELP[k] + '"></div>';
  370. }
  371. elmt += '</div>';
  372. if(k === "radio_interface_type") {
  373. elmt += '<div class="btn-group" id="radio_interface_type" data-toggle="buttons">' +
  374. '<label class="btn btn-secondary active">' +
  375. '<input type="radio" id="nrf24" name="radio_interface_type" autocomplete="off" value="nRF24" /> nRF24' +
  376. '</label>'+
  377. '<label class="btn btn-secondary">' +
  378. '<input type="radio" id="lt8900" name="radio_interface_type" autocomplete="off" value="LT8900" /> PL1167/LT8900' +
  379. '</label>' +
  380. '</div>';
  381. } else {
  382. elmt += '<input type="text" class="form-control" name="' + k + '"/>';
  383. elmt += '</div>';
  384. }
  385. settings += elmt;
  386. });
  387. $('#settings').prepend(settings);
  388. $('#settings').submit(function(e) {
  389. var obj = {};
  390. FORM_SETTINGS.forEach(function(k) {
  391. var elmt = $('#settings input[name="' + k + '"]');
  392. if (elmt.attr('type') === 'radio') {
  393. obj[k] = elmt.filter(':checked').val();
  394. } else {
  395. obj[k] = elmt.val();
  396. }
  397. });
  398. // pretty hacky. whatever.
  399. obj.device_ids = _.map(
  400. $('.selectize-control .option'),
  401. function(x) {
  402. return $(x).data('value')
  403. }
  404. );
  405. $.ajax(
  406. "/settings",
  407. {
  408. method: 'put',
  409. contentType: 'application/json',
  410. data: JSON.stringify(obj)
  411. }
  412. );
  413. e.preventDefault();
  414. return false;
  415. });
  416. $('#gateway-server-form').submit(function(e) {
  417. saveGatewayConfigs();
  418. e.preventDefault();
  419. return false;
  420. });
  421. $('.field-help').each(function() {
  422. var elmt = $('<i></i>')
  423. .addClass('glyphicon glyphicon-question-sign')
  424. .tooltip({
  425. placement: 'top',
  426. title: $(this).data('help-text'),
  427. container: 'body'
  428. });
  429. $(this).append(elmt);
  430. });
  431. $('#updates-btn').click(handleCheckForUpdates);
  432. loadSettings();
  433. updateModeOptions();
  434. });