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.

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