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.

765 lines
21 KiB

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