script.js 14 KB

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