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 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 loadTimeZones() { var time_zones = [ -720, -660, -600, -570, -540, -480, -420, -360, -300, -240, -210, -180, -120, -60, 0, 60, 120, 180, 210, 240, 270, 300, 330, 345, 360, 390, 420, 480, 510, 525, 540, 570, 600, 630, 660, 720, 765, 780, 840 ]; for (var i in time_zones) { var tz = time_zones[i]; var offset = tz >= 0 ? tz : -tz; var text = "GMT" + (tz >= 0 ? "+" : "-") + zeroPad(parseInt(offset / 60, 10), 2) + ":" + zeroPad(offset % 60, 2); $("select[name='ntpOffset']").append( $("") .attr("value", tz) .text(text) ); } } 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,31}(?= 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("Switch #" + i) ); } } // ----------------------------------------------------------------------------- // Sensors & Magnitudes // ----------------------------------------------------------------------------- function initMagnitudes(data) { // check if already initialized (each magnitude is inside div.pure-g) var done = $("#magnitudes > div").length; if (done > 0) { return; } var size = data.size; // add templates var template = $("#magnitudeTemplate").children(); for (var i=0; i // ----------------------------------------------------------------------------- // Curtains // ----------------------------------------------------------------------------- //Create the controls for one curtain. It is called when curtain is updated (so created the first time) //Let this there as we plan to have more than one curtain per switch function initCurtain(data) { var current = $("#curtains > div").length; if (current > 0) { return; } // add curtain template (prepare multi switches) var template = $("#curtainTemplate").children(); var line = $(template).clone(); // init curtain button $(line).find(".button-curtain-open").on("click", function() { sendAction("curtainAction", {button: 1}); $(this).css('background', 'red'); }); $(line).find(".button-curtain-pause").on("click", function() { sendAction("curtainAction", {button: 0}); $(this).css('background', 'red'); }); $(line).find(".button-curtain-close").on("click", function() { sendAction("curtainAction", {button: 2}); $(this).css('background', 'red'); }); line.appendTo("#curtains"); // init curtain slider $("#curtainSet").on("change", function() { var value = $(this).val(); var parent = $(this).parents(".pure-g"); $("span", parent).html(value); sendAction("curtainAction", {position: value}); }); } function initCurtainConfig(data) { var current = $("#curtainConfig > legend").length; // there is a legend per relay if (current > 0) { return; } // Populate the curtain select $("select.iscurtain").append( $("") .attr("value", "0") .text("Curtain #" + "0") ); } // ----------------------------------------------------------------------------- // Lights // ----------------------------------------------------------------------------- // wheelColorPicker accepts: // hsv(0...360,0...1,0...1) // hsv(0...100%,0...100%,0...100%) // While we use: // hsv(0...360,0...100%,0...100%) function _hsv_round(value) { return Math.round(value * 100) / 100; } function getPickerRGB(picker) { return $(picker).wheelColorPicker("getValue", "css"); } function setPickerRGB(picker, value) { $(picker).wheelColorPicker("setValue", value, true); } // TODO: use pct values instead of doing conversion? function getPickerHSV(picker) { var color = $(picker).wheelColorPicker("getColor"); return String(Math.ceil(_hsv_round(color.h) * 360)) + "," + String(Math.ceil(_hsv_round(color.s) * 100)) + "," + String(Math.ceil(_hsv_round(color.v) * 100)); } function setPickerHSV(picker, value) { if (value === getPickerHSV(picker)) return; var chunks = value.split(","); $(picker).wheelColorPicker("setColor", { h: _hsv_round(chunks[0] / 360), s: _hsv_round(chunks[1] / 100), v: _hsv_round(chunks[2] / 100) }); } function initColor(cfg) { var rgb = false; if (typeof cfg === "object") { rgb = cfg.rgb; } // check if already initialized var done = $("#colors > div").length; if (done > 0) { return; } // add template var template = $("#colorTemplate").children(); var line = $(template).clone(); line.appendTo("#colors"); // init color wheel $("input[name='color']").wheelColorPicker({ sliders: (rgb ? "wrgbp" : "whsp") }).on("sliderup", function() { if (rgb) { sendAction("color", {rgb: getPickerRGB(this)}); } else { sendAction("color", {hsv: getPickerHSV(this)}); } }); } function initCCT() { // check if already initialized var done = $("#cct > div").length; if (done > 0) { return; } $("#miredsTemplate").children().clone().appendTo("#cct"); $("#mireds").on("change", function() { var value = $(this).val(); var parent = $(this).parents(".pure-g"); $("span", parent).html(value); sendAction("mireds", {mireds: value}); }); } function initChannels(num) { // check if already initialized var done = $("#channels > div").length > 0; if (done) { return; } // does it have color channels? var colors = $("#colors > div").length > 0; // calculate channels to create var max = num; if (colors) { max = num % 3; if ((max > 0) & useWhite) { max--; if (useCCT) { max--; } } } var start = num - max; var onChannelSliderChange = function() { var id = $(this).attr("data"); var value = $(this).val(); var parent = $(this).parents(".pure-g"); $("span", parent).html(value); sendAction("channel", {id: id, value: value}); }; // add channel templates var i = 0; var template = $("#channelTemplate").children(); for (i=0; i").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($("