var debug = false; var websock; var password = false; var maxNetworks; var messages = []; var free_size = 0; var urls = {}; var numChanged = 0; var numReboot = 0; var numReconnect = 0; var numReload = 0; var configurationSaved = false; var ws_pingpong; var useWhite = false; var useCCT = false; var now = 0; var ago = 0; var packets; var filters = []; var Magnitudes = []; var MagnitudeErrors = {}; var MagnitudeNames = {}; var MagnitudeTypePrefixes = {}; var MagnitudePrefixTypes = {}; // ----------------------------------------------------------------------------- // Messages // ----------------------------------------------------------------------------- function initMessages() { messages[1] = "Remote update started"; messages[2] = "OTA update started"; messages[3] = "Error parsing data!"; messages[4] = "The file does not look like a valid configuration backup or is corrupted"; messages[5] = "Changes saved. You should reboot your board now"; messages[7] = "Passwords do not match!"; messages[8] = "Changes saved"; messages[9] = "No changes detected"; messages[10] = "Session expired, please reload page..."; } // ----------------------------------------------------------------------------- // Utils // ----------------------------------------------------------------------------- $.fn.enterKey = function (fnc) { return this.each(function () { $(this).keypress(function (ev) { var keycode = parseInt(ev.keyCode ? ev.keyCode : ev.which, 10); if (13 === keycode) { return fnc.call(this, ev); } }); }); }; function followScroll(id, threshold) { if (threshold === undefined) { threshold = 90; } var elem = document.getElementById(id); var offset = (elem.scrollTop + elem.offsetHeight) / elem.scrollHeight * 100; if (offset > threshold) { elem.scrollTop = elem.scrollHeight; } } function fromSchema(source, schema) { if (schema.length !== source.length) { throw "Schema mismatch!"; } var target = {}; schema.forEach(function(key, index) { target[key] = source[index]; }); return target; } function keepTime() { $("span[name='ago']").html(ago); ago++; if (0 === now) { return; } var date = new Date(now * 1000); var text = date.toISOString().substring(0, 19).replace("T", " "); $("input[name='now']").val(text); $("span[name='now']").html(text); now++; } function zeroPad(number, positions) { return number.toString().padStart(positions, "0"); } function validatePassword(password) { // http://www.the-art-of-web.com/javascript/validate-password/ // at least one lowercase and one uppercase letter or number // at least eight characters (letters, numbers or special characters) // MUST be 8..63 printable ASCII characters. See: // https://en.wikipedia.org/wiki/Wi-Fi_Protected_Access#Target_users_(authentication_key_distribution) // https://github.com/xoseperez/espurna/issues/1151 var re_password = /^(?=.*[A-Z\d])(?=.*[a-z])[\w~!@#$%^&*\(\)<>,.\?;:{}\[\]\\|]{8,63}$/; return ( (password !== undefined) && (typeof password === "string") && (password.length > 0) && re_password.test(password) ); } function validateFormPasswords(form) { var passwords = $("input[name='adminPass1'],input[name='adminPass2']", form); var adminPass1 = passwords.first().val(), adminPass2 = passwords.last().val(); var formValidity = passwords.first()[0].checkValidity(); if (formValidity && (adminPass1.length === 0) && (adminPass2.length === 0)) { return true; } var validPass1 = validatePassword(adminPass1), validPass2 = validatePassword(adminPass2); if (formValidity && validPass1 && validPass2) { return true; } if (!formValidity || (adminPass1.length > 0 && !validPass1)) { alert("The password you have entered is not valid, it must be 8..63 characters and have at least 1 lowercase and 1 uppercase / number!"); } if (adminPass1 !== adminPass2) { alert("Passwords are different!"); } return false; } function validateFormHostname(form) { // RFCs mandate that a hostname's labels may contain only // the ASCII letters 'a' through 'z' (case-insensitive), // the digits '0' through '9', and the hyphen. // Hostname labels cannot begin or end with a hyphen. // No other symbols, punctuation characters, or blank spaces are permitted. // Negative lookbehind does not work in Javascript // var re_hostname = new RegExp('^(?!-)[A-Za-z0-9-]{1,32}(?= 0; } function getValue(element) { if ($(element).attr("type") === "checkbox") { return $(element).prop("checked") ? 1 : 0; } else if ($(element).attr("type") === "radio") { if (!$(element).prop("checked")) { return null; } } return $(element).val(); } function addValue(data, name, value) { if (name in data) { if (!Array.isArray(data[name])) { data[name] = [data[name]]; } data[name].push(value); } else if (isGroupValue(name)) { data[name] = [value]; } else { data[name] = value; } } function getData(form, changed, cleanup) { // Populate two sets of data, ones that had been changed and ones that stayed the same var data = {}; var changed_data = []; if (cleanup === undefined) { cleanup = true; } if (changed === undefined) { changed = true; } $("input,select", form).each(function() { if ($(this).attr("data-settings-ignore") === "true") { return; } var name = $(this).attr("name"); var real_name = $(this).attr("data-settings-real-name"); if (real_name !== undefined) { name = real_name; } var value = getValue(this); if (null !== value) { var haschanged = ("true" === $(this).attr("hasChanged")); var indexed = changed_data.indexOf(name) >= 0; if ((haschanged || !changed) && !indexed) { changed_data.push(name); } addValue(data, name, value); } }); // Finally, filter out only fields that had changed. // Note: We need to preserve dynamic lists like schedules, wifi etc. // so we don't accidentally break when user deletes entry in the middle var resulting_data = {}; for (var value in data) { if (changed_data.indexOf(value) >= 0) { resulting_data[value] = data[value]; } } // Hack: clean-up leftover arrays. // When empty, the receiving side will prune all keys greater than the current one. if (cleanup) { $(".group-settings").each(function() { var haschanged = ("true" === $(this).attr("hasChanged")); if (haschanged && !this.children.length) { var targets = this.dataset.settingsTarget; if (targets === undefined) return; targets.split(" ").forEach(function(target) { resulting_data[target] = []; }); } }); } return resulting_data; } function randomString(length, args) { if (typeof args === "undefined") { args = { lowercase: true, uppercase: true, numbers: true, special: true } } var mask = ""; if (args.lowercase) { mask += "abcdefghijklmnopqrstuvwxyz"; } if (args.uppercase) { mask += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; } if (args.numbers || args.hex) { mask += "0123456789"; } if (args.hex) { mask += "ABCDEF"; } if (args.special) { mask += "~`!@#$%^&*()_+-={}[]:\";'<>?,./|\\"; } var source = new Uint32Array(length); var result = new Array(length); window.crypto.getRandomValues(source).forEach(function(value, i) { result[i] = mask[value % mask.length]; }); return result.join(""); } function generateAPIKey() { var apikey = randomString(16, {hex: true}); $("input[name='apiKey']") .val(apikey) .attr("original", "-".repeat(16)) .attr("haschanged", "true"); return false; } function generatePassword() { var password = ""; do { password = randomString(10); } while (!validatePassword(password)); return password; } function toggleVisiblePassword() { var elem = this.previousElementSibling; if (elem.type === "password") { elem.type = "text"; } else { elem.type = "password"; } return false; } function doGeneratePassword() { var elems = $("input", $("#formPassword")); elems .val(generatePassword()) .attr("haschanged", "true") .each(function() { this.type = "text"; }); return false; } function getJson(str) { try { return JSON.parse(str); } catch (e) { return false; } } function moduleVisible(module) { if (module == "sch") { $("li.module-" + module).css("display", "inherit"); $("div.module-" + module).css("display", "flex"); return; } $(".module-" + module).css("display", "inherit"); } function checkTempRangeMin() { var min = parseInt($("#tempRangeMinInput").val(), 10); var max = parseInt($("#tempRangeMaxInput").val(), 10); if (min > max - 1) { $("#tempRangeMinInput").val(max - 1); } } function checkTempRangeMax() { var min = parseInt($("#tempRangeMinInput").val(), 10); var max = parseInt($("#tempRangeMaxInput").val(), 10); if (max < min + 1) { $("#tempRangeMaxInput").val(min + 1); } } function doResetThermostatCounters(ask) { var question = (typeof ask === "undefined" || false === ask) ? null : "Are you sure you want to reset burning counters?"; return doAction(question, "thermostat_reset_counters"); } function initSelectGPIO(select) { // TODO: properly lock used GPIOs via locking and apply the mask here var mapping = [ [153, "NONE"], [0, "0 (FLASH)"], [1, "1 (U0TXD)"], [2, "2 (U1TXD)"], [3, "3 (U0RXD)"], [4, "4 (SDA)"], [5, "5 (SCL)"], [9, "9 (SDD2)"], [10, "10 (SDD3)"], [12, "12 (MTDI)"], [13, "13 (MTCK)"], [14, "14 (MTMS)"], [15, "15 (MTDO)"], [16, "16 (WAKE)"], ]; for (n in mapping) { var elem = $('").attr("value",i).text("Channel #" + i)); } // add brightness template var template = $("#brightnessTemplate").children(); var line = $(template).clone(); line.appendTo("#channels"); // init bright slider $("#brightness").on("change", function() { var value = $(this).val(); var parent = $(this).parents(".pure-g"); $("span", parent).html(value); sendAction("brightness", {value: value}); }); } // ----------------------------------------------------------------------------- // RFBridge // ----------------------------------------------------------------------------- function rfbLearn() { var parent = $(this).parents(".pure-g"); var input = $("input", parent); sendAction("rfblearn", {id: input.attr("data-id"), status: input.attr("data-status")}); } function rfbForget() { var parent = $(this).parents(".pure-g"); var input = $("input", parent); sendAction("rfbforget", {id: input.attr("data-id"), status: input.attr("data-status")}); } function rfbSend() { var parent = $(this).parents(".pure-g"); var input = $("input", parent); sendAction("rfbsend", {id: input.attr("data-id"), status: input.attr("data-status"), data: input.val()}); } function addRfbNode() { var numNodes = $("#rfbNodes > legend").length; var template = $("#rfbNodeTemplate").children(); var line = $(template).clone(); $("span", line).html(numNodes); $(line).find("input").each(function() { this.dataset["id"] = numNodes; }); $(line).find(".button-rfb-learn").on("click", rfbLearn); $(line).find(".button-rfb-forget").on("click", rfbForget); $(line).find(".button-rfb-send").on("click", rfbSend); line.appendTo("#rfbNodes"); return line; } // ----------------------------------------------------------------------------- // LightFox // ----------------------------------------------------------------------------- function lightfoxLearn() { sendAction("lightfoxLearn", {}); } function lightfoxClear() { sendAction("lightfoxClear", {}); } function initLightfox(data, relayCount) { var numNodes = data.length; var template = $("#lightfoxNodeTemplate").children(); var i, j; for (i=0; i span").text(data[i]["id"]); $line.find("select").each(function() { $(this).attr("name", "btnRelay" + data[i]["id"]); for (j=0; j < relayCount; j++) { $(this).append($("