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.

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