mirror of
https://github.com/xoseperez/espurna.git
synced 2026-03-03 23:14:15 +01:00
inverse logic for 'get everything' agrument (currently, only external use) search specific forms vs. all document when performing cleanup more explicit types, allow imports
469 lines
10 KiB
JavaScript
469 lines
10 KiB
JavaScript
/// <reference path="index.build.d.mts" />
|
|
|
|
import { notifyError } from './errors.mjs';
|
|
window.onerror = notifyError;
|
|
|
|
import {
|
|
pageReloadIn,
|
|
randomString,
|
|
showPanel,
|
|
styleInject,
|
|
} from './core.mjs';
|
|
|
|
import { validatePassword, validateFormsPasswords } from './validate.mjs';
|
|
|
|
import {
|
|
askAndCallAction,
|
|
askAndCallReboot,
|
|
askAndCallReconnect,
|
|
} from './question.mjs';
|
|
|
|
import {
|
|
init as initSettings,
|
|
applySettings,
|
|
getData,
|
|
setChangedElement,
|
|
updateVariables,
|
|
variableListeners,
|
|
} from './settings.mjs';
|
|
|
|
import { init as initWiFi } from './wifi.mjs';
|
|
import { init as initGpio } from './gpio.mjs';
|
|
import { init as initConnection, connect } from './connection.mjs';
|
|
|
|
import { init as initApi } from './api.mjs';
|
|
import { init as initCurtain } from './curtain.mjs';
|
|
import { init as initDebug } from './debug.mjs';
|
|
import { init as initDomoticz } from './domoticz.mjs';
|
|
import { init as initGarland } from './garland.mjs';
|
|
import { init as initHa } from './ha.mjs';
|
|
import { init as initLed } from './led.mjs';
|
|
import { init as initLight } from './light.mjs';
|
|
import { init as initLightfox } from './lightfox.mjs';
|
|
import { init as initOta } from './ota.mjs';
|
|
import { init as initRelay } from './relay.mjs';
|
|
import { init as initRfm69 } from './rfm69.mjs';
|
|
import { init as initRfbridge } from './rfbridge.mjs';
|
|
import { init as initRules } from './rules.mjs';
|
|
import { init as initSchedule } from './schedule.mjs';
|
|
import { init as initSensor } from './sensor.mjs';
|
|
import { init as initThermostat } from './thermostat.mjs';
|
|
import { init as initThingspeak } from './thingspeak.mjs';
|
|
|
|
/** @type {number | null} */
|
|
let KeepTime = null;
|
|
|
|
/** @type {number} */
|
|
let Ago = 0;
|
|
|
|
/**
|
|
* @typedef {{date: Date | null, offset: string}} NowType
|
|
* @type NowType
|
|
*/
|
|
const Now = {
|
|
date: null,
|
|
offset: "",
|
|
};
|
|
|
|
/**
|
|
* @type {{[k: string]: string}}
|
|
*/
|
|
const __title_cache = {
|
|
hostname: "?",
|
|
app_name: "ESPurna",
|
|
app_version: "0.0.0",
|
|
};
|
|
|
|
/**
|
|
* @param {string} key
|
|
* @param {string} value
|
|
*/
|
|
function documentTitle(key, value) {
|
|
__title_cache[key] = value;
|
|
document.title = `${__title_cache.hostname} - ${__title_cache.app_name} ${__title_cache.app_version}`;
|
|
}
|
|
|
|
/**
|
|
* @param {string} module
|
|
*/
|
|
function moduleVisible(module) {
|
|
styleInject([`.module-${module} { display: revert; }`]);
|
|
}
|
|
|
|
/**
|
|
* @param {string[]} modules
|
|
*/
|
|
function modulesVisible(modules) {
|
|
modules.forEach((module) => {
|
|
moduleVisible(module);
|
|
});
|
|
}
|
|
|
|
function modulesVisibleAll() {
|
|
document.querySelectorAll("[class*=module-]")
|
|
.forEach((elem) => {
|
|
/** @type {HTMLElement} */(elem).style.display = "revert";
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {string} value
|
|
*/
|
|
function deviceNow(value) {
|
|
try {
|
|
Now.date = normalizedDate(value);
|
|
Now.offset = timestampOffset(value);
|
|
} catch (e) {
|
|
notifyError(null, null, 0, 0, e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} value
|
|
*/
|
|
function onAction(value) {
|
|
if ("reload" === value) {
|
|
pageReloadIn(1000);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} value
|
|
*/
|
|
function onMessage(value) {
|
|
window.alert(value);
|
|
}
|
|
|
|
/**
|
|
* @param {number} value
|
|
*/
|
|
function initWebMode(value) {
|
|
const initial = (1 === value);
|
|
|
|
const layout = document.getElementById("layout")
|
|
if (layout) {
|
|
layout.style.display = (initial ? "none" : "inherit");
|
|
}
|
|
|
|
const password = document.getElementById("password");
|
|
if (password) {
|
|
password.style.display = initial ? "inherit" : "none";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} value
|
|
* @returns {string}
|
|
*/
|
|
function timestampDatetime(value) {
|
|
return value.slice(0, 19);
|
|
}
|
|
|
|
/**
|
|
* @param {string} value
|
|
* @returns {string}
|
|
*/
|
|
function timestampOffset(value) {
|
|
if (value.endsWith("Z")) {
|
|
return "Z";
|
|
}
|
|
|
|
return value.slice(-6);
|
|
}
|
|
|
|
/**
|
|
* @param {NowType} now
|
|
* @returns {string}
|
|
*/
|
|
function displayDatetime(now) {
|
|
if (now.date) {
|
|
let datetime = timestampDatetime(now.date.toISOString());
|
|
datetime = datetime.replace("T", " ");
|
|
datetime = `${datetime} ${now.offset}`;
|
|
return datetime;
|
|
}
|
|
|
|
return "?";
|
|
}
|
|
|
|
/**
|
|
* @param {string} value
|
|
* @returns {string}
|
|
*/
|
|
function normalizedTimestamp(value) {
|
|
return `${timestampDatetime(value)}Z`;
|
|
}
|
|
|
|
/**
|
|
* @param {string} value
|
|
* @returns {Date}
|
|
*/
|
|
function normalizedDate(value) {
|
|
return new Date(normalizedTimestamp(value));
|
|
}
|
|
|
|
|
|
function keepTime() {
|
|
const ago = document.querySelector("span[data-key='app:ago']");
|
|
if (ago) {
|
|
ago.textContent = Ago.toString();
|
|
}
|
|
|
|
++Ago;
|
|
|
|
if (null !== Now.date) {
|
|
const now = document.querySelector("span[data-key='app:now']");
|
|
if (now) {
|
|
now.textContent = displayDatetime(Now);
|
|
}
|
|
|
|
Now.date = new Date(Now.date.valueOf() + 1000);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns {import("./settings.mjs").KeyValueListeners}
|
|
*/
|
|
function listeners() {
|
|
return {
|
|
"action": (_, value) => {
|
|
onAction(value);
|
|
},
|
|
"app_name": documentTitle,
|
|
"app_version": documentTitle,
|
|
"hostname": documentTitle,
|
|
"message": (_, value) => {
|
|
onMessage(value);
|
|
},
|
|
"modulesVisible": (_, value) => {
|
|
modulesVisible(value);
|
|
},
|
|
"now": (_, value) => {
|
|
deviceNow(value);
|
|
},
|
|
"webMode": (_, value) => {
|
|
initWebMode(value);
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @returns {string}
|
|
*/
|
|
function generatePassword() {
|
|
let password = "";
|
|
do {
|
|
password = randomString(10);
|
|
} while (!validatePassword(password));
|
|
|
|
return password;
|
|
}
|
|
|
|
/**
|
|
* @param {HTMLFormElement} form
|
|
*/
|
|
function generatePasswordsForForm(form) {
|
|
const value = generatePassword();
|
|
|
|
for (let name of ["adminPass0", "adminPass1"]) {
|
|
const elem = form.elements.namedItem(name);
|
|
if (elem && elem instanceof HTMLInputElement) {
|
|
setChangedElement(elem);
|
|
elem.type = "text";
|
|
elem.value = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {HTMLFormElement} form
|
|
*/
|
|
function initSetupPassword(form) {
|
|
document.querySelector(".button-setup-password")
|
|
?.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
const forms = [form];
|
|
if (validateFormsPasswords(forms, true)) {
|
|
applySettings(getData(forms, {cleanup: true}));
|
|
}
|
|
});
|
|
document.querySelector(".button-generate-password")
|
|
?.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
generatePasswordsForForm(form);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {Event} event
|
|
* @returns {any}
|
|
*/
|
|
function toggleMenu(event) {
|
|
event.preventDefault();
|
|
/** @type {HTMLElement} */(event.target).parentElement?.classList.toggle("active");
|
|
}
|
|
|
|
/**
|
|
* @param {Event} event
|
|
*/
|
|
function toggleVisiblePassword(event) {
|
|
const target = /** @type {HTMLSpanElement} */(event.target);
|
|
const input = /** @type {HTMLInputElement} */(target.previousElementSibling);
|
|
|
|
if (input.type === "password") {
|
|
input.type = "text";
|
|
} else {
|
|
input.type = "password";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {MessageEvent<any>} event
|
|
*/
|
|
function onJsonPayload(event) {
|
|
Ago = 0;
|
|
|
|
if (!KeepTime) {
|
|
KeepTime = window.setInterval(keepTime, 1000);
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(
|
|
event.data
|
|
.replace(/\n/g, "\\n")
|
|
.replace(/\r/g, "\\r")
|
|
.replace(/\t/g, "\\t"));
|
|
updateVariables(parsed);
|
|
} catch (e) {
|
|
notifyError(null, null, 0, 0, e);
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
// Initial page, when webMode only allows to change the password
|
|
const passwd = document.forms.namedItem("form-setup-password");
|
|
if (passwd) {
|
|
initSetupPassword(passwd);
|
|
}
|
|
|
|
document.querySelectorAll(".password-reveal")
|
|
.forEach((elem) => {
|
|
elem.addEventListener("click", toggleVisiblePassword);
|
|
});
|
|
|
|
// Sidebar menu & buttons
|
|
document.querySelector(".menu-link")
|
|
?.addEventListener("click", toggleMenu);
|
|
document.querySelectorAll(".pure-menu-link")
|
|
.forEach((elem) => {
|
|
elem.addEventListener("click", showPanel);
|
|
});
|
|
|
|
document.querySelector(".button-reconnect")
|
|
?.addEventListener("click", askAndCallReconnect);
|
|
document.querySelectorAll(".button-reboot")
|
|
.forEach((elem) => {
|
|
elem.addEventListener("click", askAndCallReboot);
|
|
});
|
|
|
|
// Generic action sender
|
|
document.querySelectorAll(".button-simple-action")
|
|
.forEach((elem) => {
|
|
elem.addEventListener("click", askAndCallAction);
|
|
});
|
|
|
|
variableListeners(listeners());
|
|
|
|
initConnection();
|
|
initSettings();
|
|
initWiFi();
|
|
initGpio();
|
|
|
|
if (MODULE_OTA) {
|
|
initOta();
|
|
}
|
|
|
|
if (MODULE_HA) {
|
|
initHa();
|
|
}
|
|
|
|
if (MODULE_SNS) {
|
|
initSensor();
|
|
}
|
|
|
|
if (MODULE_GARLAND) {
|
|
initGarland();
|
|
}
|
|
|
|
if (MODULE_THERMOSTAT) {
|
|
initThermostat();
|
|
}
|
|
|
|
if (MODULE_LIGHTFOX) {
|
|
initLightfox();
|
|
}
|
|
|
|
if (MODULE_RELAY) {
|
|
initRelay();
|
|
}
|
|
|
|
if (MODULE_RFM69) {
|
|
initRfm69();
|
|
}
|
|
|
|
if (MODULE_RFB) {
|
|
initRfbridge();
|
|
}
|
|
|
|
if (MODULE_CMD || MODULE_DBG) {
|
|
initDebug();
|
|
}
|
|
|
|
if (MODULE_API) {
|
|
initApi();
|
|
}
|
|
|
|
if (MODULE_LED) {
|
|
initLed();
|
|
}
|
|
|
|
if (MODULE_LIGHT) {
|
|
initLight();
|
|
}
|
|
|
|
if (MODULE_SCH) {
|
|
initSchedule();
|
|
}
|
|
|
|
if (MODULE_RPN) {
|
|
initRules();
|
|
}
|
|
|
|
if (MODULE_RELAY && MODULE_DCZ) {
|
|
initDomoticz();
|
|
}
|
|
|
|
if (MODULE_RELAY && MODULE_TSPK) {
|
|
initThingspeak();
|
|
}
|
|
|
|
if (MODULE_CURTAIN) {
|
|
initCurtain();
|
|
}
|
|
|
|
// don't autoconnect w/ localhost or file://
|
|
if (MODULE_LOCAL) {
|
|
updateVariables({
|
|
webMode: 0,
|
|
now: "2024-01-01T00:00:00+01:00",
|
|
});
|
|
KeepTime = window.setInterval(keepTime, 1000);
|
|
modulesVisibleAll();
|
|
return;
|
|
}
|
|
|
|
connect(onJsonPayload);
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", init);
|