Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

774 lines
22 KiB

8 years ago
7 years ago
7 years ago
8 years ago
8 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
  1. var websock;
  2. var password = false;
  3. var maxNetworks;
  4. var useWhite = false;
  5. var messages = [];
  6. var webhost;
  7. // http://www.the-art-of-web.com/javascript/validate-password/
  8. function checkPassword(str) {
  9. // at least one number, one lowercase and one uppercase letter
  10. // at least eight characters that are letters, numbers or the underscore
  11. var re = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])\w{8,}$/;
  12. return re.test(str);
  13. }
  14. function validateForm(form) {
  15. // password
  16. var adminPass1 = $("input[name='adminPass1']", form).val();
  17. if (adminPass1.length > 0 && !checkPassword(adminPass1)) {
  18. alert("The password you have entered is not valid, it must have at least 8 characters, 1 lower and 1 uppercase and 1 number!");
  19. return false;
  20. }
  21. var adminPass2 = $("input[name='adminPass2']", form).val();
  22. if (adminPass1 != adminPass2) {
  23. alert("Passwords are different!");
  24. return false;
  25. }
  26. return true;
  27. }
  28. function valueSet(data, name, value) {
  29. for (var i in data) {
  30. if (data[i]['name'] == name) {
  31. data[i]['value'] = value;
  32. return;
  33. }
  34. }
  35. data.push({'name': name, 'value': value});
  36. }
  37. function zeroPad(number, positions) {
  38. return ("0".repeat(positions) + number).slice(-positions);
  39. }
  40. function doUpdate() {
  41. var form = $("#formSave");
  42. if (validateForm(form)) {
  43. // Get data
  44. var data = form.serializeArray();
  45. // Post-process
  46. delete(data['filename']);
  47. $("input[type='checkbox']").each(function() {
  48. var name = $(this).attr("name");
  49. if (name) {
  50. valueSet(data, name, $(this).is(':checked') ? 1 : 0);
  51. }
  52. });
  53. websock.send(JSON.stringify({'config': data}));
  54. $(".hlwExpected").val(0);
  55. $("input[name='hlwExpectedReset']")
  56. .prop("checked", false)
  57. .iphoneStyle("refresh");
  58. }
  59. return false;
  60. }
  61. function doUpgrade() {
  62. var contents = $("input[name='upgrade']")[0].files[0];
  63. if (typeof contents == 'undefined') {
  64. alert("First you have to select a file from your computer.");
  65. return false;
  66. }
  67. var filename = $("input[name='upgrade']").val().split('\\').pop();
  68. var data = new FormData();
  69. data.append('upgrade', contents, filename);
  70. $.ajax({
  71. // Your server script to process the upload
  72. url: webhost + 'upgrade',
  73. type: 'POST',
  74. // Form data
  75. data: data,
  76. // Tell jQuery not to process data or worry about content-type
  77. // You *must* include these options!
  78. cache: false,
  79. contentType: false,
  80. processData: false,
  81. success: function(data, text) {
  82. $("#upgrade-progress").hide();
  83. if (data == 'OK') {
  84. alert("Firmware image uploaded, board rebooting. This page will be refreshed in 5 seconds.");
  85. setTimeout(function() {
  86. window.location.reload();
  87. }, 5000);
  88. } else {
  89. alert("There was an error trying to upload the new image, please try again (" + data + ").");
  90. }
  91. },
  92. // Custom XMLHttpRequest
  93. xhr: function() {
  94. $("#upgrade-progress").show();
  95. var myXhr = $.ajaxSettings.xhr();
  96. if (myXhr.upload) {
  97. // For handling the progress of the upload
  98. myXhr.upload.addEventListener('progress', function(e) {
  99. if (e.lengthComputable) {
  100. $('progress').attr({ value: e.loaded, max: e.total });
  101. }
  102. } , false);
  103. }
  104. return myXhr;
  105. },
  106. });
  107. return false;
  108. }
  109. function doUpdatePassword() {
  110. var form = $("#formPassword");
  111. if (validateForm(form)) {
  112. var data = form.serializeArray();
  113. websock.send(JSON.stringify({'config': data}));
  114. }
  115. return false;
  116. }
  117. function doReset() {
  118. var response = window.confirm("Are you sure you want to reset the device?");
  119. if (response == false) return false;
  120. websock.send(JSON.stringify({'action': 'reset'}));
  121. return false;
  122. }
  123. function doReconnect() {
  124. var response = window.confirm("Are you sure you want to disconnect from the current WIFI network?");
  125. if (response == false) return false;
  126. websock.send(JSON.stringify({'action': 'reconnect'}));
  127. return false;
  128. }
  129. function doToggle(element, value) {
  130. var relayID = parseInt(element.attr("data"));
  131. websock.send(JSON.stringify({'action': 'relay', 'data': { 'id': relayID, 'status': value ? 1 : 0 }}));
  132. return false;
  133. }
  134. function backupSettings() {
  135. document.getElementById('downloader').src = webhost + 'config';
  136. return false;
  137. }
  138. function onFileUpload(event) {
  139. var inputFiles = this.files;
  140. if (inputFiles == undefined || inputFiles.length == 0) return false;
  141. var inputFile = inputFiles[0];
  142. this.value = "";
  143. var response = window.confirm("Previous settings will be overwritten. Are you sure you want to restore this settings?");
  144. if (response == false) return false;
  145. var reader = new FileReader();
  146. reader.onload = function(e) {
  147. var data = getJson(e.target.result);
  148. if (data) {
  149. websock.send(JSON.stringify({'action': 'restore', 'data': data}));
  150. } else {
  151. alert(messages[4]);
  152. }
  153. };
  154. reader.readAsText(inputFile);
  155. return false;
  156. }
  157. function restoreSettings() {
  158. if (typeof window.FileReader !== 'function') {
  159. alert("The file API isn't supported on this browser yet.");
  160. } else {
  161. $("#uploader").click();
  162. }
  163. return false;
  164. }
  165. function randomString(length, chars) {
  166. var mask = '';
  167. if (chars.indexOf('a') > -1) mask += 'abcdefghijklmnopqrstuvwxyz';
  168. if (chars.indexOf('A') > -1) mask += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
  169. if (chars.indexOf('#') > -1) mask += '0123456789';
  170. if (chars.indexOf('@') > -1) mask += 'ABCDEF';
  171. if (chars.indexOf('!') > -1) mask += '~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\';
  172. var result = '';
  173. for (var i = length; i > 0; --i) result += mask[Math.round(Math.random() * (mask.length - 1))];
  174. return result;
  175. }
  176. function doGenerateAPIKey() {
  177. var apikey = randomString(16, '@#');
  178. $("input[name=\"apiKey\"]").val(apikey);
  179. return false;
  180. }
  181. function showPanel() {
  182. $(".panel").hide();
  183. $("#" + $(this).attr("data")).show();
  184. if ($("#layout").hasClass('active')) toggleMenu();
  185. $("input[type='checkbox']").iphoneStyle("calculateDimensions").iphoneStyle("refresh");
  186. };
  187. function toggleMenu() {
  188. $("#layout").toggleClass('active');
  189. $("#menu").toggleClass('active');
  190. $("#menuLink").toggleClass('active');
  191. }
  192. function createRelays(count) {
  193. var current = $("#relays > div").length;
  194. if (current > 0) return;
  195. var template = $("#relayTemplate .pure-g")[0];
  196. for (var relayID=0; relayID<count; relayID++) {
  197. var line = $(template).clone();
  198. $(line).find("input").each(function() {
  199. $(this).attr("data", relayID);
  200. });
  201. if (count > 1) $(".relay_id", line).html(" " + (relayID+1));
  202. line.appendTo("#relays");
  203. $(":checkbox", line).iphoneStyle({
  204. onChange: doToggle,
  205. resizeContainer: true,
  206. resizeHandle: true,
  207. checkedLabel: 'ON',
  208. uncheckedLabel: 'OFF'
  209. });
  210. }
  211. }
  212. function createIdxs(count) {
  213. var current = $("#idxs > div").length;
  214. if (current > 0) return;
  215. var template = $("#idxTemplate .pure-g")[0];
  216. for (var id=0; id<count; id++) {
  217. var line = $(template).clone();
  218. $(line).find("input").each(function() {
  219. $(this).attr("data", id).attr("tabindex", 40+id);
  220. });
  221. if (count > 1) $(".id", line).html(" " + id);
  222. line.appendTo("#idxs");
  223. }
  224. }
  225. function delNetwork() {
  226. var parent = $(this).parents(".pure-g");
  227. $(parent).remove();
  228. }
  229. function moreNetwork() {
  230. var parent = $(this).parents(".pure-g");
  231. $("div.more", parent).toggle();
  232. }
  233. function addNetwork() {
  234. var numNetworks = $("#networks > div").length;
  235. if (numNetworks >= maxNetworks) {
  236. alert("Max number of networks reached");
  237. return;
  238. }
  239. var tabindex = 200 + numNetworks * 10;
  240. var template = $("#networkTemplate").children();
  241. var line = $(template).clone();
  242. $(line).find("input").each(function() {
  243. $(this).attr("tabindex", tabindex++);
  244. });
  245. $(line).find(".button-del-network").on('click', delNetwork);
  246. $(line).find(".button-more-network").on('click', moreNetwork);
  247. line.appendTo("#networks");
  248. return line;
  249. }
  250. function initColor() {
  251. // check if already initialized
  252. var done = $("#colors > div").length;
  253. if (done > 0) return;
  254. // add template
  255. var template = $("#colorTemplate").children();
  256. var line = $(template).clone();
  257. line.appendTo("#colors");
  258. // init color wheel
  259. $('input[name="color"]').wheelColorPicker({
  260. sliders: 'wrgbp'
  261. }).on('sliderup', function() {
  262. var value = $(this).wheelColorPicker('getValue', 'css');
  263. websock.send(JSON.stringify({'action': 'color', 'data' : value}));
  264. });
  265. // init bright slider
  266. noUiSlider.create($("#brightness").get(0), {
  267. start: 255,
  268. connect: [true, false],
  269. tooltips: true,
  270. format: {
  271. to: function (value) { return parseInt(value); },
  272. from: function (value) { return value; }
  273. },
  274. orientation: "horizontal",
  275. range: { 'min': 0, 'max': 255}
  276. }).on("change", function() {
  277. var value = parseInt(this.get());
  278. websock.send(JSON.stringify({'action': 'brightness', 'data' : value}));
  279. });
  280. }
  281. function initChannels(num) {
  282. // check if already initialized
  283. var done = $("#channels > div").length > 0;
  284. if (done) return;
  285. // does it have color channels?
  286. var colors = $("#colors > div").length > 0;
  287. // calculate channels to create
  288. var max = num;
  289. if (colors) {
  290. max = num % 3;
  291. if ((max > 0) & useWhite) max--;
  292. }
  293. var start = num - max;
  294. // add templates
  295. var template = $("#channelTemplate").children();
  296. for (var i=0; i<max; i++) {
  297. var channel_id = start + i;
  298. var line = $(template).clone();
  299. $(".slider", line).attr("data", channel_id);
  300. $("label", line).html("Channel " + (channel_id + 1));
  301. noUiSlider.create($(".slider", line).get(0), {
  302. start: 0,
  303. connect: [true, false],
  304. tooltips: true,
  305. format: {
  306. to: function (value) { return parseInt(value); },
  307. from: function (value) { return value; }
  308. },
  309. orientation: "horizontal",
  310. range: { 'min': 0, 'max': 255 }
  311. }).on("change", function() {
  312. var id = $(this.target).attr("data");
  313. var value = parseInt(this.get());
  314. websock.send(JSON.stringify({'action': 'channel', 'data': { 'id': id, 'value': value }}));
  315. });
  316. line.appendTo("#channels");
  317. }
  318. }
  319. function addRfbNode() {
  320. var numNodes = $("#rfbNodes > fieldset").length;
  321. var template = $("#rfbNodeTemplate").children();
  322. var line = $(template).clone();
  323. var status = true;
  324. $("span", line).html(numNodes+1);
  325. $(line).find("input").each(function() {
  326. $(this).attr("data_id", numNodes);
  327. $(this).attr("data_status", status ? 1 : 0);
  328. status = !status;
  329. });
  330. $(line).find(".button-rfb-learn").on('click', rfbLearn);
  331. $(line).find(".button-rfb-forget").on('click', rfbForget);
  332. $(line).find(".button-rfb-send").on('click', rfbSend);
  333. line.appendTo("#rfbNodes");
  334. return line;
  335. }
  336. function rfbLearn() {
  337. var parent = $(this).parents(".pure-g");
  338. var input = $("input", parent);
  339. websock.send(JSON.stringify({'action': 'rfblearn', 'data' : {'id' : input.attr("data_id"), 'status': input.attr("data_status")}}));
  340. }
  341. function rfbForget() {
  342. var parent = $(this).parents(".pure-g");
  343. var input = $("input", parent);
  344. websock.send(JSON.stringify({'action': 'rfbforget', 'data' : {'id' : input.attr("data_id"), 'status': input.attr("data_status")}}));
  345. }
  346. function rfbSend() {
  347. var parent = $(this).parents(".pure-g");
  348. var input = $("input", parent);
  349. websock.send(JSON.stringify({'action': 'rfbsend', 'data' : {'id' : input.attr("data_id"), 'status': input.attr("data_status"), 'data': input.val()}}));
  350. }
  351. function forgetCredentials() {
  352. $.ajax({
  353. 'method': 'GET',
  354. 'url': '/',
  355. 'async': false,
  356. 'username': "logmeout",
  357. 'password': "123456",
  358. 'headers': { "Authorization": "Basic xxx" }
  359. }).done(function(data) {
  360. return false;
  361. // If we don't get an error, we actually got an error as we expect an 401!
  362. }).fail(function(){
  363. // We expect to get an 401 Unauthorized error! In this case we are successfully
  364. // logged out and we redirect the user.
  365. return true;
  366. });
  367. }
  368. function processData(data) {
  369. // title
  370. if ("app" in data) {
  371. var title = data.app;
  372. if ("version" in data) {
  373. title = title + " " + data.version;
  374. }
  375. $(".pure-menu-heading").html(title);
  376. if ("hostname" in data) {
  377. title = data.hostname + " - " + title;
  378. }
  379. document.title = title;
  380. }
  381. Object.keys(data).forEach(function(key) {
  382. // Web Modes
  383. if (key == "webMode") {
  384. password = data.webMode == 1;
  385. $("#layout").toggle(data.webMode == 0);
  386. $("#password").toggle(data.webMode == 1);
  387. $("#credentials").hide();
  388. }
  389. // Actions
  390. if (key == "action") {
  391. if (data.action == "reload") {
  392. if (password) forgetCredentials();
  393. setTimeout(function() {
  394. window.location.reload();
  395. }, 1000);
  396. }
  397. if (data.action == "rfbLearn") {
  398. // Nothing to do?
  399. }
  400. if (data.action == "rfbTimeout") {
  401. // Nothing to do?
  402. }
  403. return;
  404. }
  405. if (key == "rfbCount") {
  406. for (var i=0; i<data.rfbCount; i++) addRfbNode();
  407. return;
  408. }
  409. if (key == "rfb") {
  410. var nodes = data.rfb;
  411. for (var i in nodes) {
  412. var node = nodes[i];
  413. var element = $("input[name=rfbcode][data_id=" + node["id"] + "][data_status=" + node["status"] + "]");
  414. if (element.length) element.val(node["data"]);
  415. }
  416. return;
  417. }
  418. if (key == "color") {
  419. initColor();
  420. $("input[name='color']").wheelColorPicker('setValue', data[key], true);
  421. return;
  422. }
  423. if (key == "brightness") {
  424. var slider = $("#brightness");
  425. if (slider.length) slider.get(0).noUiSlider.set(data[key]);
  426. return;
  427. }
  428. if (key == "channels") {
  429. var len = data[key].length;
  430. initChannels(len);
  431. for (var i=0; i<len; i++) {
  432. var slider = $("div.channels[data=" + i + "]");
  433. if (slider.length) slider.get(0).noUiSlider.set(data[key][i]);
  434. }
  435. return;
  436. }
  437. if (key == "uptime") {
  438. var uptime = parseInt(data[key]);
  439. var seconds = uptime % 60; uptime = parseInt(uptime / 60);
  440. var minutes = uptime % 60; uptime = parseInt(uptime / 60);
  441. var hours = uptime % 24; uptime = parseInt(uptime / 24);
  442. var days = uptime;
  443. data[key] = days + 'd ' + zeroPad(hours, 2) + 'h ' + zeroPad(minutes, 2) + 'm ' + zeroPad(seconds, 2) + 's';
  444. }
  445. if (key == "useWhite") {
  446. useWhite = data[key];
  447. }
  448. if (key == "maxNetworks") {
  449. maxNetworks = parseInt(data.maxNetworks);
  450. return;
  451. }
  452. // Wifi
  453. if (key == "wifi") {
  454. var networks = data.wifi;
  455. for (var i in networks) {
  456. // add a new row
  457. var line = addNetwork();
  458. // fill in the blanks
  459. var wifi = data.wifi[i];
  460. Object.keys(wifi).forEach(function(key) {
  461. var element = $("input[name=" + key + "]", line);
  462. if (element.length) element.val(wifi[key]);
  463. });
  464. }
  465. return;
  466. }
  467. // Relay status
  468. if (key == "relayStatus") {
  469. var relays = data.relayStatus;
  470. createRelays(relays.length);
  471. for (var relayID in relays) {
  472. var element = $(".relayStatus[data=" + relayID + "]");
  473. if (element.length > 0) {
  474. element
  475. .prop("checked", relays[relayID])
  476. .iphoneStyle("refresh");
  477. }
  478. }
  479. return;
  480. }
  481. // Domoticz
  482. if (key == "dczRelayIdx") {
  483. var idxs = data.dczRelayIdx;
  484. createIdxs(idxs.length);
  485. for (var i in idxs) {
  486. var element = $(".dczRelayIdx[data=" + i + "]");
  487. if (element.length > 0) element.val(idxs[i]);
  488. }
  489. return;
  490. }
  491. // Messages
  492. if (key == "message") {
  493. window.alert(messages[data.message]);
  494. return;
  495. }
  496. // Enable options
  497. if (key.endsWith("Visible")) {
  498. var module = key.slice(0,-7);
  499. $(".module-" + module).show();
  500. return;
  501. }
  502. // Pre-process
  503. if (key == "network") {
  504. data.network = data.network.toUpperCase();
  505. }
  506. if (key == "mqttStatus") {
  507. data.mqttStatus = data.mqttStatus ? "CONNECTED" : "NOT CONNECTED";
  508. }
  509. if (key == "ntpStatus") {
  510. data.ntpStatus = data.ntpStatus ? "SYNC'D" : "NOT SYNC'D";
  511. }
  512. if (key == "tmpUnits") {
  513. $("span#tmpUnit").html(data[key] == 1 ? "ºF" : "ºC");
  514. }
  515. // Look for INPUTs
  516. var element = $("input[name=" + key + "]");
  517. if (element.length > 0) {
  518. if (element.attr('type') == 'checkbox') {
  519. element
  520. .prop("checked", data[key])
  521. .iphoneStyle("refresh");
  522. } else if (element.attr('type') == 'radio') {
  523. element.val([data[key]]);
  524. } else {
  525. var pre = element.attr("pre") || "";
  526. var post = element.attr("post") || "";
  527. element.val(pre + data[key] + post);
  528. }
  529. return;
  530. }
  531. // Look for SPANs
  532. var element = $("span[name=" + key + "]");
  533. if (element.length > 0) {
  534. var pre = element.attr("pre") || "";
  535. var post = element.attr("post") || "";
  536. element.html(pre + data[key] + post);
  537. return;
  538. }
  539. // Look for SELECTs
  540. var element = $("select[name=" + key + "]");
  541. if (element.length > 0) {
  542. element.val(data[key]);
  543. return;
  544. }
  545. });
  546. // Auto generate an APIKey if none defined yet
  547. if ($("input[name='apiKey']").val() == "") {
  548. doGenerateAPIKey();
  549. }
  550. }
  551. function getJson(str) {
  552. try {
  553. return JSON.parse(str);
  554. } catch (e) {
  555. return false;
  556. }
  557. }
  558. function connect(host) {
  559. if (typeof host === 'undefined') {
  560. host = window.location.href.replace('#', '');
  561. } else {
  562. if (!host.startsWith("http")) {
  563. host = 'http://' + host + '/';
  564. }
  565. }
  566. webhost = host;
  567. wshost = host.replace('http', 'ws') + 'ws';
  568. if (websock) websock.close();
  569. websock = new WebSocket(wshost);
  570. websock.onopen = function(evt) {
  571. console.log("Connected");
  572. };
  573. websock.onclose = function(evt) {
  574. console.log("Disconnected");
  575. };
  576. websock.onerror = function(evt) {
  577. console.log("Error: ", evt);
  578. };
  579. websock.onmessage = function(evt) {
  580. var data = getJson(evt.data);
  581. if (data) processData(data);
  582. };
  583. }
  584. function initMessages() {
  585. messages[01] = "Remote update started";
  586. messages[02] = "OTA update started";
  587. messages[03] = "Error parsing data!";
  588. messages[04] = "The file does not look like a valid configuration backup or is corrupted";
  589. messages[05] = "Changes saved. You should reboot your board now";
  590. messages[06] = "Home Assistant auto-discovery message sent";
  591. messages[07] = "Passwords do not match!";
  592. messages[08] = "Changes saved";
  593. messages[09] = "No changes detected";
  594. messages[10] = "Session expired, please reload page...";
  595. }
  596. function init() {
  597. initMessages();
  598. $("#menuLink").on('click', toggleMenu);
  599. $(".button-update").on('click', doUpdate);
  600. $(".button-update-password").on('click', doUpdatePassword);
  601. $(".button-reset").on('click', doReset);
  602. $(".button-reconnect").on('click', doReconnect);
  603. $(".button-settings-backup").on('click', backupSettings);
  604. $(".button-settings-restore").on('click', restoreSettings);
  605. $('#uploader').on('change', onFileUpload);
  606. $(".button-apikey").on('click', doGenerateAPIKey);
  607. $(".button-upgrade").on('click', doUpgrade);
  608. $(".button-upgrade-browse").on('click', function() {
  609. $("input[name='upgrade']")[0].click();
  610. return false;
  611. });
  612. $("input[name='upgrade']").change(function (){
  613. var fileName = $(this).val();
  614. $("input[name='filename']").val(fileName.replace(/^.*[\\\/]/, ''));
  615. });
  616. $('progress').attr({ value: 0, max: 100 });
  617. $(".pure-menu-link").on('click', showPanel);
  618. $(".button-add-network").on('click', function() {
  619. $("div.more", addNetwork()).toggle();
  620. });
  621. $(".button-ha-send").on('click', function() {
  622. websock.send(JSON.stringify({'action': 'ha_send', 'data': $("input[name='haPrefix']").val()}));
  623. });
  624. $.ajax({
  625. 'method': 'GET',
  626. 'url': window.location.href + 'auth'
  627. }).done(function(data) {
  628. connect();
  629. }).fail(function(){
  630. $("#credentials").show();
  631. });
  632. }
  633. $(init);