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,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("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($("