feat: multi webpage support #25
This commit is contained in:
+735
@@ -0,0 +1,735 @@
|
||||
const os = require("os");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const fsp = require("fs/promises");
|
||||
const cpr = require("child_process");
|
||||
|
||||
global.HARDWARE = global.HARDWARE || {
|
||||
initialized: false,
|
||||
status: null,
|
||||
session: {},
|
||||
support: {},
|
||||
display: {
|
||||
status: {
|
||||
path: null,
|
||||
value: null,
|
||||
},
|
||||
brightness: {
|
||||
path: null,
|
||||
value: null,
|
||||
max: null,
|
||||
},
|
||||
notifiers: [],
|
||||
},
|
||||
keyboard: {
|
||||
visible: null,
|
||||
notifiers: [],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the hardware with the provided arguments.
|
||||
*
|
||||
* @returns {bool} Returns true if the initialization was successful.
|
||||
*/
|
||||
const init = async () => {
|
||||
if (!compatibleDevice()) {
|
||||
console.warn("Device not supported");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Init globals
|
||||
HARDWARE.session.user = sessionUser();
|
||||
HARDWARE.session.type = sessionType();
|
||||
HARDWARE.display.status.path = getDisplayStatusPath();
|
||||
HARDWARE.support.displayStatus = HARDWARE.display.status.path !== null;
|
||||
HARDWARE.display.brightness.path = getDisplayBrightnessPath();
|
||||
HARDWARE.support.displayBrightness = HARDWARE.display.brightness.path !== null;
|
||||
HARDWARE.display.brightness.max = getDisplayBrightnessMax();
|
||||
HARDWARE.support.keyboardVisibility = processRuns("squeekboard");
|
||||
HARDWARE.initialized = true;
|
||||
HARDWARE.status = "Supported";
|
||||
|
||||
// Show supported features
|
||||
console.log(`Supported: ${JSON.stringify(HARDWARE.support, null, 2)}`);
|
||||
|
||||
// Show session infos
|
||||
console.log("\nUser:", HARDWARE.session.user);
|
||||
console.log("Session:", HARDWARE.session.type);
|
||||
|
||||
// Show device infos
|
||||
console.log("\nModel:", getModel());
|
||||
console.log("Vendor:", getVendor());
|
||||
console.log("Serial Number:", getSerialNumber());
|
||||
console.log("Host Name:", getHostName());
|
||||
|
||||
// Show system infos
|
||||
console.log("\nUp Time:", getUpTime());
|
||||
console.log("Memory Size:", getMemorySize());
|
||||
console.log("Memory Usage:", getMemoryUsage());
|
||||
console.log("Processor Usage:", getProcessorUsage());
|
||||
console.log("Processor Temperature:", getProcessorTemperature());
|
||||
|
||||
// Show display infos
|
||||
console.log(`\nDisplay Status [${HARDWARE.display.status.path}]:`, getDisplayStatus());
|
||||
console.log(`Display Brightness [${HARDWARE.display.brightness.path}]:`, getDisplayBrightness(), "\n");
|
||||
|
||||
// Check for keyboard visibility
|
||||
setKeyboardVisibility("OFF", (reply, error) => {
|
||||
if (!reply || error) {
|
||||
return;
|
||||
}
|
||||
dbusMonitor("/sm/puri/OSK0", (property, error) => {
|
||||
if (!property || error) {
|
||||
return;
|
||||
}
|
||||
HARDWARE.keyboard.visibility = property.Visible === "true";
|
||||
HARDWARE.keyboard.notifiers.forEach((notifier) => {
|
||||
notifier();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Check for display changes
|
||||
interval(update, 1000);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the shared hardware properties.
|
||||
*/
|
||||
const update = async () => {
|
||||
if (!HARDWARE.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Display status has changed
|
||||
if (HARDWARE.support.displayStatus) {
|
||||
const status = (await fsp.readFile(`${HARDWARE.display.status.path}/dpms`, "utf8")).trim();
|
||||
if (status !== HARDWARE.display.status.value) {
|
||||
console.log("Update Display Status:", getDisplayStatus());
|
||||
HARDWARE.display.status.value = status;
|
||||
HARDWARE.display.notifiers.forEach((notifier) => {
|
||||
notifier();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Display brightness has changed
|
||||
if (HARDWARE.support.displayBrightness) {
|
||||
const brightness = (await fsp.readFile(`${HARDWARE.display.brightness.path}/brightness`, "utf8")).trim();
|
||||
if (brightness !== HARDWARE.display.brightness.value) {
|
||||
console.log("Update Display Brightness:", getDisplayBrightness());
|
||||
HARDWARE.display.brightness.value = brightness;
|
||||
HARDWARE.display.notifiers.forEach((notifier) => {
|
||||
notifier();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies device compatibility by checking the presence of necessary sys paths.
|
||||
*
|
||||
* @returns {bool} Returns true if all paths exists.
|
||||
*/
|
||||
const compatibleDevice = () => {
|
||||
if (os.platform() !== "linux") {
|
||||
return false;
|
||||
}
|
||||
const paths = ["/sys/class/drm", "/sys/class/backlight", "/sys/class/thermal"];
|
||||
return paths.every((path) => fs.existsSync(path));
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the session user name using `os.userInfo()`.
|
||||
*
|
||||
* @returns {string|null} Returns session user name or null if an error occurs.
|
||||
*/
|
||||
const sessionUser = () => {
|
||||
try {
|
||||
return os.userInfo().username;
|
||||
} catch {}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the session type for the user using `loginctl`.
|
||||
*
|
||||
* @returns {string|null} Returns session type 'x11'/'wayland' or null if an error occurs.
|
||||
*/
|
||||
const sessionType = () => {
|
||||
if (!commandExists("loginctl")) {
|
||||
return null;
|
||||
}
|
||||
return execSyncCommand("loginctl", [
|
||||
"show-session",
|
||||
"$(loginctl show-user $(whoami) -p Display --value)",
|
||||
"-p Type --value",
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks the session type for the logged in user.
|
||||
*
|
||||
* @returns {bool} Returns true if the session matches the type.
|
||||
*/
|
||||
const session = (type) => {
|
||||
return HARDWARE.session.type === type;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the model name using `/sys/firmware/devicetree/base/model` or `/sys/class/dmi/id/product_name`.
|
||||
*
|
||||
* @returns {string} The model name of the device or 'Generic' if not found.
|
||||
*/
|
||||
const getModel = () => {
|
||||
const paths = ["/sys/firmware/devicetree/base/model", "/sys/class/dmi/id/product_name"];
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
return execSyncCommand("sudo", ["cat", path]) || "Generic";
|
||||
}
|
||||
}
|
||||
return "Generic";
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the vendor name using `/sys/class/dmi/id/board_vendor`.
|
||||
*
|
||||
* @returns {string} The vendor name of the device or 'Generic' if not found.
|
||||
*/
|
||||
const getVendor = () => {
|
||||
const paths = ["/sys/class/dmi/id/board_vendor"];
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
return execSyncCommand("sudo", ["cat", path]) || "Generic";
|
||||
}
|
||||
}
|
||||
const model = getModel();
|
||||
if (model.includes("Raspberry Pi")) {
|
||||
return "Raspberry Pi Ltd";
|
||||
}
|
||||
return "Generic";
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the serial number using `/sys/firmware/devicetree/base/serial-number` or `/sys/class/dmi/id/product_serial`.
|
||||
*
|
||||
* @returns {string} The serial number of the device or '123456' if not found.
|
||||
*/
|
||||
const getSerialNumber = () => {
|
||||
const paths = ["/sys/firmware/devicetree/base/serial-number", "/sys/class/dmi/id/product_serial"];
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
return execSyncCommand("sudo", ["cat", path]) || "123456";
|
||||
}
|
||||
}
|
||||
return "123456";
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the host machine id using `/etc/machine-id`.
|
||||
*
|
||||
* @returns {string} The machine id of the system or '123456' if not found.
|
||||
*/
|
||||
const getMachineId = () => {
|
||||
const paths = ["/etc/machine-id"];
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
return execSyncCommand("sudo", ["cat", path]) || "123456";
|
||||
}
|
||||
}
|
||||
return "123456";
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the host name of the current system using `os.hostname()`.
|
||||
*
|
||||
* @returns {string} The host name of the system.
|
||||
*/
|
||||
const getHostName = () => {
|
||||
return os.hostname();
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the up time of the system in minutes using `os.uptime()`.
|
||||
*
|
||||
* @returns {number} The up time of the system in minutes.
|
||||
*/
|
||||
const getUpTime = () => {
|
||||
return os.uptime() / 60;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the total available memory in gibibytes using `os.totalmem()`.
|
||||
*
|
||||
* @returns {number} The total available memory in GiB.
|
||||
*/
|
||||
const getMemorySize = () => {
|
||||
return os.totalmem() / 1024 ** 3;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current memory usage as a percentage using `os.totalmem()` and `os.freemem()`.
|
||||
*
|
||||
* @returns {number} The percentage of used memory.
|
||||
*/
|
||||
const getMemoryUsage = () => {
|
||||
return ((os.totalmem() - os.freemem()) / os.totalmem()) * 100;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the CPU load average over the last 5 minutes as a percentage using `os.loadavg()` and `os.cpus()`.
|
||||
*
|
||||
* @returns {number} The CPU load average percentage over the last 5 minutes.
|
||||
*/
|
||||
const getProcessorUsage = () => {
|
||||
return (os.loadavg()[1] / os.cpus().length) * 100;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current CPU temperature using `/sys/class/thermal`.
|
||||
*
|
||||
* @returns {number|null} The CPU temperature in degrees celsius or null if nothing was found.
|
||||
*/
|
||||
const getProcessorTemperature = () => {
|
||||
const thermal = "/sys/class/thermal";
|
||||
for (const zone of fs.readdirSync(thermal)) {
|
||||
const typeFile = path.join(thermal, zone, "type");
|
||||
const tempFile = path.join(thermal, zone, "temp");
|
||||
if (!fs.existsSync(typeFile) || !fs.existsSync(tempFile)) {
|
||||
continue;
|
||||
}
|
||||
const type = fs.readFileSync(typeFile, "utf8").trim();
|
||||
if (["cpu-thermal", "x86_pkg_temp", "k10temp", "acpitz"].includes(type)) {
|
||||
return parseFloat(fs.readFileSync(tempFile, "utf8").trim()) / 1000;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current display status path using `/sys/class/drm`.
|
||||
*
|
||||
* @returns {string|null} The display status path or null if nothing was found.
|
||||
*/
|
||||
const getDisplayStatusPath = () => {
|
||||
const drm = "/sys/class/drm";
|
||||
for (const card of fs.readdirSync(drm)) {
|
||||
const statusFile = path.join(drm, card, "status");
|
||||
if (!fs.existsSync(statusFile)) {
|
||||
continue;
|
||||
}
|
||||
const content = fs.readFileSync(statusFile, "utf8").trim();
|
||||
if (content === "connected") {
|
||||
return path.join(drm, card);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current display power status using `wlopm` or `xset`.
|
||||
*
|
||||
* @returns {string|null} The display status as 'ON'/'OFF' or null if an error occurs.
|
||||
*/
|
||||
const getDisplayStatus = () => {
|
||||
if (!HARDWARE.support.displayStatus) {
|
||||
return null;
|
||||
}
|
||||
if (session("wayland") && commandExists("wlopm")) {
|
||||
const status = execSyncCommand("wlopm", []);
|
||||
if (status !== null) {
|
||||
const out = status.split("\n")[0].split(" ");
|
||||
return out.pop().toUpperCase();
|
||||
}
|
||||
} else if (session("x11") && commandExists("xset")) {
|
||||
const status = execSyncCommand("xset", ["-q"]);
|
||||
if (status !== null) {
|
||||
const on = /Monitor is On/.test(status);
|
||||
return on ? "ON" : "OFF";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the display power status using `wlopm` or `xset`.
|
||||
*
|
||||
* This function takes a desired status ('ON' or 'OFF') and executes
|
||||
* the appropriate command to set the display status.
|
||||
*
|
||||
* @param {string} status - The desired status ('ON' or 'OFF').
|
||||
* @param {Function} callback - A callback function that receives the output or error.
|
||||
*/
|
||||
const setDisplayStatus = (status, callback = null) => {
|
||||
if (!HARDWARE.support.displayStatus) {
|
||||
if (typeof callback === "function") callback(null, "Not supported");
|
||||
return;
|
||||
}
|
||||
if (!["ON", "OFF"].includes(status)) {
|
||||
console.error("Status must be 'ON' or 'OFF'");
|
||||
if (typeof callback === "function") callback(null, "Invalid status");
|
||||
return;
|
||||
}
|
||||
if (session("wayland") && commandExists("wlopm")) {
|
||||
execAsyncCommand("wlopm", [`--${status.toLowerCase()}`, "*"], callback);
|
||||
} else if (session("x11") && commandExists("xset")) {
|
||||
execAsyncCommand("xset", ["dpms", "force", status.toLowerCase()], callback);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current display brightness path using `/sys/class/backlight`.
|
||||
*
|
||||
* @returns {string|null} The display brightness path or null if nothing was found.
|
||||
*/
|
||||
const getDisplayBrightnessPath = () => {
|
||||
const backlight = "/sys/class/backlight";
|
||||
for (const address of fs.readdirSync(backlight)) {
|
||||
const brightnessFile = path.join(backlight, address, "brightness");
|
||||
if (!fs.existsSync(brightnessFile)) {
|
||||
continue;
|
||||
}
|
||||
return path.join(backlight, address);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the maximum display brightness value using `/sys/class/backlight/.../max_brightness`.
|
||||
*
|
||||
* @returns {number|null} The brightness maximum value or null if an error occurs.
|
||||
*/
|
||||
const getDisplayBrightnessMax = () => {
|
||||
if (!HARDWARE.support.displayBrightness) {
|
||||
return null;
|
||||
}
|
||||
const max = execSyncCommand("cat", [`${HARDWARE.display.brightness.path}/max_brightness`]);
|
||||
if (max !== null) {
|
||||
return parseInt(max, 10);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current display brightness level using `/sys/class/backlight/.../brightness`.
|
||||
*
|
||||
* @returns {number|null} The brightness level as a percentage or null if an error occurs.
|
||||
*/
|
||||
const getDisplayBrightness = () => {
|
||||
if (!HARDWARE.support.displayBrightness) {
|
||||
return null;
|
||||
}
|
||||
const brightness = execSyncCommand("cat", [`${HARDWARE.display.brightness.path}/brightness`]);
|
||||
if (brightness !== null) {
|
||||
const max = HARDWARE.display.brightness.max;
|
||||
return Math.round((parseInt(brightness, 10) / max) * 100);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the display brightness level using `/sys/class/backlight/.../brightness`.
|
||||
*
|
||||
* This function takes a brightness value between 1 to 100 percent,
|
||||
* maps it to the proper range and writes it to the system.
|
||||
*
|
||||
* @param {number} brightness - The desired brightness level (1-100).
|
||||
* @param {Function} callback - A callback function that receives the output or error.
|
||||
*/
|
||||
const setDisplayBrightness = (brightness, callback = null) => {
|
||||
if (!HARDWARE.support.displayBrightness) {
|
||||
if (typeof callback === "function") callback(null, "Not supported");
|
||||
return;
|
||||
}
|
||||
if (typeof brightness !== "number" || brightness < 1 || brightness > 100) {
|
||||
console.error("Brightness must be a number between 1 and 100");
|
||||
if (typeof callback === "function") callback(null, "Invalid brightness");
|
||||
return;
|
||||
}
|
||||
const max = HARDWARE.display.brightness.max;
|
||||
const value = Math.max(1, Math.min(Math.round((brightness / 100) * max), max));
|
||||
const proc = execAsyncCommand("sudo", ["tee", `${HARDWARE.display.brightness.path}/brightness`], callback);
|
||||
proc.stdin.write(value.toString());
|
||||
proc.stdin.end();
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the keyboard visibility using global properties.
|
||||
*
|
||||
* @returns {string|null} The keyboard visibility as 'ON'/'OFF' or null if an error occurs.
|
||||
*/
|
||||
const getKeyboardVisibility = () => {
|
||||
if (!HARDWARE.support.keyboardVisibility) {
|
||||
return null;
|
||||
}
|
||||
return HARDWARE.keyboard.visibility ? "ON" : "OFF";
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the keyboard visibility using `dbus-send`.
|
||||
*
|
||||
* This function takes a desired visibility ('ON' or 'OFF') and executes
|
||||
* the appropriate command to show or hide the keyboard.
|
||||
*
|
||||
* @param {bool} visibility - The desired visibility ('ON' or 'OFF').
|
||||
* @param {Function} callback - A callback function that receives the output or error.
|
||||
*/
|
||||
const setKeyboardVisibility = (visibility, callback = null) => {
|
||||
if (!HARDWARE.support.keyboardVisibility) {
|
||||
if (typeof callback === "function") callback(null, "Not supported");
|
||||
return;
|
||||
}
|
||||
if (!["ON", "OFF"].includes(visibility)) {
|
||||
console.error("Visibility must be 'ON' or 'OFF'");
|
||||
if (typeof callback === "function") callback(null, "Invalid visibility");
|
||||
return;
|
||||
}
|
||||
const visible = visibility === "ON";
|
||||
HARDWARE.keyboard.visibility = visible;
|
||||
dbusCall("/sm/puri/OSK0", "SetVisible", [`boolean:${visible}`], callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if system upgrades are available using `apt`.
|
||||
*
|
||||
* @returns {Array<string>} A list of package names that are available for upgrade.
|
||||
*/
|
||||
const checkPackageUpgrades = () => {
|
||||
if (!commandExists("apt")) {
|
||||
return [];
|
||||
}
|
||||
const output = execSyncCommand("apt", ["list", "--upgradable", "2>/dev/null"]);
|
||||
const packages = (output || "").trim().split("\n");
|
||||
packages.shift();
|
||||
return packages;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shuts down the system using `sudo shutdown -h now`.
|
||||
*
|
||||
* This function executes the command asynchronously.
|
||||
* The output of the command will be provided through the callback function.
|
||||
*
|
||||
* @param {Function} callback - A callback function that receives the output or error.
|
||||
*/
|
||||
const shutdownSystem = (callback = null) => {
|
||||
execAsyncCommand("sudo", ["shutdown", "-h", "now"], callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reboots the system using `sudo reboot`.
|
||||
*
|
||||
* This function executes the command asynchronously.
|
||||
* The output of the command will be provided through the callback function.
|
||||
*
|
||||
* @param {Function} callback - A callback function that receives the output or error.
|
||||
*/
|
||||
const rebootSystem = (callback = null) => {
|
||||
execAsyncCommand("sudo", ["reboot"], callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a process is running using `pidof`.
|
||||
*
|
||||
* @param {string} name - The process name to check.
|
||||
* @returns {bool} Returns true if the process runs.
|
||||
*/
|
||||
const processRuns = (name) => {
|
||||
try {
|
||||
return !!cpr.execSync(`pidof ${name}`, { encoding: "utf8" });
|
||||
} catch {}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a command is available using `which`.
|
||||
*
|
||||
* @param {string} name - The command name to check.
|
||||
* @returns {bool} Returns true if the command is available.
|
||||
*/
|
||||
const commandExists = (name) => {
|
||||
try {
|
||||
return !!cpr.execSync(`which ${name}`, { encoding: "utf8" });
|
||||
} catch {}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes a command synchronously and returns the output.
|
||||
*
|
||||
* @param {string} cmd - The command to execute.
|
||||
* @param {Array<string>} args - The arguments for the command.
|
||||
* @returns {string|null} The output of the command or null if an error occurs.
|
||||
*/
|
||||
const execSyncCommand = (cmd, args = []) => {
|
||||
try {
|
||||
const output = cpr.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
|
||||
return output.trim().replace(/\0/g, "");
|
||||
} catch (error) {
|
||||
console.error("Execute Sync:", error.message);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes a command asynchronously.
|
||||
*
|
||||
* @param {string} cmd - The command to execute.
|
||||
* @param {Array<string>} args - The arguments for the command.
|
||||
* @param {Function} callback - A callback function that receives the output or error.
|
||||
* @returns {Object} The spawned process object.
|
||||
*/
|
||||
const execAsyncCommand = (cmd, args = [], callback = null) => {
|
||||
let errorOutput = "";
|
||||
let successOutput = "";
|
||||
let proc = cpr.spawn(cmd, args);
|
||||
proc.stderr.on("data", (data) => {
|
||||
if (data) {
|
||||
errorOutput += data.toString();
|
||||
}
|
||||
});
|
||||
proc.stdout.on("data", (data) => {
|
||||
if (data) {
|
||||
successOutput += data.toString();
|
||||
}
|
||||
});
|
||||
proc.on("close", (code) => {
|
||||
try {
|
||||
if (typeof callback === "function") {
|
||||
if (code === 0) {
|
||||
callback(successOutput.trim().replace(/\0/g, ""), null);
|
||||
} else {
|
||||
callback(null, errorOutput.trim().replace(/\0/g, ""));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Execute Async:", error.message);
|
||||
if (typeof callback === "function") {
|
||||
callback(null, error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
return proc;
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes a D-Bus method call synchronously using `dbus-send`.
|
||||
*
|
||||
* @param {string} path - The D-Bus object path.
|
||||
* @param {string} method - The D-Bus method name.
|
||||
* @param {Array<string>} values - The argument values for the D-Bus method.
|
||||
* @param {Function} callback - A callback function that receives the output or error.
|
||||
*/
|
||||
const dbusCall = (path, method, values, callback = null) => {
|
||||
const cmd = "dbus-send";
|
||||
const iface = path.slice(1).replace(/\//g, ".");
|
||||
const dest = `${iface} ${path} ${iface}.${method} ${values.join(" ")}`;
|
||||
const args = ["--print-reply", "--type=method_call", `--dest=${dest}`];
|
||||
try {
|
||||
const output = cpr.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
|
||||
if (typeof callback === "function") {
|
||||
callback(output.trim().replace(/\0/g, ""), null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Call D-Bus:", error.message);
|
||||
if (typeof callback === "function") {
|
||||
callback(null, error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Monitors D-Bus property changes asynchronously using `dbus-monitor`.
|
||||
*
|
||||
* @param {string} path - The D-Bus object path.
|
||||
* @param {Function} callback - A callback function that receives the changed property.
|
||||
* @returns {Object} The spawned process object.
|
||||
*/
|
||||
const dbusMonitor = (path, callback) => {
|
||||
const cmd = "dbus-monitor";
|
||||
const args = [`interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path='${path}'`];
|
||||
const proc = cpr.spawn(cmd, args);
|
||||
proc.stdout.on("data", (data) => {
|
||||
try {
|
||||
const signal = data.toString();
|
||||
if (signal.includes("member=PropertiesChanged") && signal.includes(`path=${path}`)) {
|
||||
const dicts = [...signal.matchAll(/dict entry\(\s*([^)]*?)\)/g)].map((dict) => dict[1].trim());
|
||||
if (dicts.length) {
|
||||
dicts.forEach((dict) => {
|
||||
const key = dict.match(/string "(.*?)"/);
|
||||
const value = dict.match(/(?<=variant\s+)(.*)/);
|
||||
if (key && value && typeof callback === "function") {
|
||||
callback({ [key[1].trim()]: value[1].trim().split(" ").pop() }, null);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback({ Visible: `${HARDWARE.keyboard.visibility}` }, null);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Monitor D-Bus:", error.message);
|
||||
if (typeof callback === "function") {
|
||||
callback(null, error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
proc.stderr.on("data", (data) => {
|
||||
if (data) {
|
||||
console.error("Monitor D-Bus:", data.toString());
|
||||
if (typeof callback === "function") {
|
||||
callback(null, data.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
return proc;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function for asynchronous interval calls.
|
||||
*
|
||||
* @param {Function} callback - An async callback function.
|
||||
* @param {number} ms - Sleep time in milliseconds.
|
||||
*/
|
||||
const interval = (callback, ms) => {
|
||||
const run = () => {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await callback();
|
||||
} catch (error) {
|
||||
console.error("Interval Callback:", error.message);
|
||||
}
|
||||
run();
|
||||
}, ms);
|
||||
};
|
||||
run();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
init,
|
||||
update,
|
||||
getModel,
|
||||
getVendor,
|
||||
getSerialNumber,
|
||||
getMachineId,
|
||||
getHostName,
|
||||
getUpTime,
|
||||
getMemorySize,
|
||||
getMemoryUsage,
|
||||
getProcessorUsage,
|
||||
getProcessorTemperature,
|
||||
getDisplayStatusPath,
|
||||
getDisplayStatus,
|
||||
setDisplayStatus,
|
||||
getDisplayBrightnessPath,
|
||||
getDisplayBrightnessMax,
|
||||
getDisplayBrightness,
|
||||
setDisplayBrightness,
|
||||
getKeyboardVisibility,
|
||||
setKeyboardVisibility,
|
||||
checkPackageUpgrades,
|
||||
shutdownSystem,
|
||||
rebootSystem,
|
||||
};
|
||||
@@ -0,0 +1,858 @@
|
||||
const mqtt = require("mqtt");
|
||||
const hardware = require("./hardware");
|
||||
const { app } = require("electron");
|
||||
|
||||
global.INTEGRATION = global.INTEGRATION || {
|
||||
initialized: false,
|
||||
status: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the integration with the provided arguments.
|
||||
*
|
||||
* @returns {bool} Returns true if the initialization was successful.
|
||||
*/
|
||||
const init = async () => {
|
||||
if (!ARGS.mqtt_url) {
|
||||
return false;
|
||||
}
|
||||
if (!/^mqtts?:\/\//.test(ARGS.mqtt_url)) {
|
||||
console.error("Please provide the '--mqtt-url' parameter with mqtt(s)");
|
||||
return app.quit();
|
||||
}
|
||||
const url = new URL(ARGS.mqtt_url);
|
||||
const user = ARGS.mqtt_user ? ARGS.mqtt_user : null;
|
||||
const password = ARGS.mqtt_password ? ARGS.mqtt_password : null;
|
||||
const discovery = ARGS.mqtt_discovery ? ARGS.mqtt_discovery : "homeassistant";
|
||||
|
||||
const model = hardware.getModel();
|
||||
const vendor = hardware.getVendor();
|
||||
const hostName = hardware.getHostName();
|
||||
const serialNumber = hardware.getSerialNumber();
|
||||
const serialNumberSuffix = serialNumber.slice(-6);
|
||||
|
||||
const deviceId = serialNumberSuffix.toUpperCase().replace(/[^A-Z0-9]/g, "");
|
||||
const deviceName = hostName.charAt(0).toUpperCase() + hostName.slice(1);
|
||||
|
||||
// Init globals
|
||||
INTEGRATION.node = `rpi_${deviceId}`;
|
||||
INTEGRATION.discovery = discovery;
|
||||
INTEGRATION.device = {
|
||||
name: `TouchKio ${deviceName}`,
|
||||
model: model,
|
||||
manufacturer: vendor,
|
||||
serial_number: serialNumber,
|
||||
identifiers: [INTEGRATION.node],
|
||||
sw_version: `${app.getName()}-v${app.getVersion()}`,
|
||||
};
|
||||
|
||||
// Connection settings
|
||||
const options = user === null || password === null ? null : { username: user, password: password };
|
||||
const masked = password === null ? "null" : "*".repeat(password.length);
|
||||
console.log("MQTT Connecting:", `${user}:${masked}@${url.toString()}`);
|
||||
INTEGRATION.client = mqtt.connect(url.toString(), options);
|
||||
|
||||
// Client connected
|
||||
INTEGRATION.client
|
||||
.on("connect", () => {
|
||||
console.log(`MQTT Connected: ${url.toString()}\n`);
|
||||
|
||||
// Init client controls
|
||||
initShutdown(INTEGRATION.client);
|
||||
initReboot(INTEGRATION.client);
|
||||
initRefresh(INTEGRATION.client);
|
||||
initKiosk(INTEGRATION.client);
|
||||
initDisplay(INTEGRATION.client);
|
||||
initKeyboard(INTEGRATION.client);
|
||||
initPageNumber(INTEGRATION.client);
|
||||
initPageUrl(INTEGRATION.client);
|
||||
|
||||
// Init client sensors
|
||||
initModel(INTEGRATION.client);
|
||||
initSerialNumber(INTEGRATION.client);
|
||||
initHostName(INTEGRATION.client);
|
||||
initUpTime(INTEGRATION.client);
|
||||
initMemorySize(INTEGRATION.client);
|
||||
initMemoryUsage(INTEGRATION.client);
|
||||
initProcessorUsage(INTEGRATION.client);
|
||||
initProcessorTemperature(INTEGRATION.client);
|
||||
initPackageUpgrades(INTEGRATION.client);
|
||||
initHeartbeat(INTEGRATION.client);
|
||||
initLastActive(INTEGRATION.client);
|
||||
|
||||
// Integration initialized
|
||||
INTEGRATION.initialized = true;
|
||||
INTEGRATION.status = "Online";
|
||||
})
|
||||
.on("error", (error) => {
|
||||
console.error("MQTT:", error);
|
||||
INTEGRATION.status = "Offline";
|
||||
});
|
||||
|
||||
// Update sensor states from notifiers
|
||||
HARDWARE.display.notifiers.push(() => {
|
||||
updateDisplay(INTEGRATION.client);
|
||||
});
|
||||
HARDWARE.keyboard.notifiers.push(() => {
|
||||
updateKeyboard(INTEGRATION.client);
|
||||
});
|
||||
|
||||
// Update time sensors periodically (30s)
|
||||
setInterval(() => {
|
||||
updateHeartbeat(INTEGRATION.client);
|
||||
updateLastActive(INTEGRATION.client);
|
||||
}, 30 * 1000);
|
||||
|
||||
// Update system sensors periodically (1min)
|
||||
setInterval(() => {
|
||||
update();
|
||||
}, 60 * 1000);
|
||||
|
||||
// Update package sensors periodically (60min)
|
||||
setInterval(() => {
|
||||
updatePackageUpgrades(INTEGRATION.client);
|
||||
}, 3600 * 1000);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the shared integration properties.
|
||||
*/
|
||||
const update = async () => {
|
||||
if (!INTEGRATION.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update client sensors
|
||||
updateKiosk(INTEGRATION.client);
|
||||
updatePageNumber(INTEGRATION.client);
|
||||
updatePageUrl(INTEGRATION.client);
|
||||
updateUpTime(INTEGRATION.client);
|
||||
updateLastActive(INTEGRATION.client);
|
||||
updateMemoryUsage(INTEGRATION.client);
|
||||
updateProcessorUsage(INTEGRATION.client);
|
||||
updateProcessorTemperature(INTEGRATION.client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the shutdown button and handles the execute logic.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initShutdown = (client) => {
|
||||
const root = `${INTEGRATION.discovery}/button/${INTEGRATION.node}/shutdown`;
|
||||
const config = {
|
||||
name: "Shutdown",
|
||||
unique_id: `${INTEGRATION.node}_shutdown`,
|
||||
command_topic: `${root}/execute`,
|
||||
icon: "mdi:power",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client
|
||||
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === `${root}/execute`) {
|
||||
console.log("Shutdown system...");
|
||||
hardware.setDisplayStatus("ON");
|
||||
hardware.shutdownSystem();
|
||||
}
|
||||
})
|
||||
.subscribe(`${root}/execute`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the reboot button and handles the execute logic.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initReboot = (client) => {
|
||||
const root = `${INTEGRATION.discovery}/button/${INTEGRATION.node}/reboot`;
|
||||
const config = {
|
||||
name: "Reboot",
|
||||
unique_id: `${INTEGRATION.node}_reboot`,
|
||||
command_topic: `${root}/execute`,
|
||||
icon: "mdi:restart",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client
|
||||
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === `${root}/execute`) {
|
||||
console.log("Rebooting system...");
|
||||
hardware.setDisplayStatus("ON");
|
||||
hardware.rebootSystem();
|
||||
}
|
||||
})
|
||||
.subscribe(`${root}/execute`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the refresh button and handles the execute logic.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initRefresh = (client) => {
|
||||
const root = `${INTEGRATION.discovery}/button/${INTEGRATION.node}/refresh`;
|
||||
const config = {
|
||||
name: "Refresh",
|
||||
unique_id: `${INTEGRATION.node}_refresh`,
|
||||
command_topic: `${root}/execute`,
|
||||
icon: "mdi:web-refresh",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client
|
||||
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === `${root}/execute`) {
|
||||
console.log("Refreshing webview...");
|
||||
hardware.setDisplayStatus("ON");
|
||||
WEBVIEW.views[WEBVIEW.viewActive].webContents.reloadIgnoringCache();
|
||||
}
|
||||
})
|
||||
.subscribe(`${root}/execute`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the kiosk select status and handles the execute logic.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initKiosk = (client) => {
|
||||
const root = `${INTEGRATION.discovery}/select/${INTEGRATION.node}/kiosk`;
|
||||
const config = {
|
||||
name: "Kiosk",
|
||||
unique_id: `${INTEGRATION.node}_kiosk`,
|
||||
command_topic: `${root}/set`,
|
||||
state_topic: `${root}/status`,
|
||||
value_template: "{{ value }}",
|
||||
options: ["Framed", "Fullscreen", "Maximized", "Minimized", "Terminated"],
|
||||
icon: "mdi:overscan",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client
|
||||
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === `${root}/set`) {
|
||||
const status = message.toString();
|
||||
console.log("Set Kiosk Status:", status);
|
||||
hardware.setDisplayStatus("ON");
|
||||
switch (status) {
|
||||
case "Framed":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
break;
|
||||
case "Fullscreen":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(true);
|
||||
break;
|
||||
case "Maximized":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
WEBVIEW.window.maximize();
|
||||
break;
|
||||
case "Minimized":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
WEBVIEW.window.minimize();
|
||||
break;
|
||||
case "Terminated":
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
})
|
||||
.subscribe(`${root}/set`);
|
||||
updateKiosk(client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the kiosk status via the mqtt connection.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const updateKiosk = (client) => {
|
||||
const kiosk = WEBVIEW.status;
|
||||
const root = `${INTEGRATION.discovery}/select/${INTEGRATION.node}/kiosk`;
|
||||
if (kiosk !== null) {
|
||||
client.publish(`${root}/status`, `${kiosk}`, { qos: 1, retain: true });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the display status, brightness and handles the execute logic.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initDisplay = (client) => {
|
||||
if (!HARDWARE.support.displayStatus) {
|
||||
return;
|
||||
}
|
||||
const root = `${INTEGRATION.discovery}/light/${INTEGRATION.node}/display`;
|
||||
const config = {
|
||||
name: "Display",
|
||||
unique_id: `${INTEGRATION.node}_display`,
|
||||
command_topic: `${root}/set`,
|
||||
state_topic: `${root}/status`,
|
||||
icon: "mdi:monitor-shimmer",
|
||||
platform: "light",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
if (HARDWARE.support.displayBrightness) {
|
||||
config.brightness_scale = 100;
|
||||
config.brightness_command_topic = `${root}/brightness/set`;
|
||||
config.brightness_state_topic = `${root}/brightness/status`;
|
||||
}
|
||||
client
|
||||
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === `${root}/set`) {
|
||||
const status = message.toString();
|
||||
console.log("Set Display Status:", status);
|
||||
hardware.setDisplayStatus(status);
|
||||
hardware.update();
|
||||
} else if (topic === `${root}/brightness/set`) {
|
||||
const brightness = parseInt(message, 10);
|
||||
console.log("Set Display Brightness:", brightness);
|
||||
hardware.setDisplayBrightness(brightness);
|
||||
hardware.update();
|
||||
}
|
||||
})
|
||||
.subscribe(`${root}/set`)
|
||||
.subscribe(`${root}/brightness/set`);
|
||||
updateDisplay(client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the display status, brightness via the mqtt connection.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const updateDisplay = (client) => {
|
||||
const status = hardware.getDisplayStatus();
|
||||
const brightness = hardware.getDisplayBrightness();
|
||||
const root = `${INTEGRATION.discovery}/light/${INTEGRATION.node}/display`;
|
||||
if (status !== null) {
|
||||
client.publish(`${root}/status`, `${status}`, { qos: 1, retain: true });
|
||||
}
|
||||
if (brightness !== null) {
|
||||
client.publish(`${root}/brightness/status`, `${brightness}`, { qos: 1, retain: true });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the keyboard visibility and handles the execute logic.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initKeyboard = (client) => {
|
||||
if (!HARDWARE.support.keyboardVisibility) {
|
||||
return;
|
||||
}
|
||||
const root = `${INTEGRATION.discovery}/switch/${INTEGRATION.node}/keyboard`;
|
||||
const config = {
|
||||
name: "Keyboard",
|
||||
unique_id: `${INTEGRATION.node}_keyboard`,
|
||||
command_topic: `${root}/set`,
|
||||
state_topic: `${root}/status`,
|
||||
icon: "mdi:keyboard-close-outline",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client
|
||||
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === `${root}/set`) {
|
||||
const status = message.toString();
|
||||
console.log("Set Keyboard Visibility:", status);
|
||||
hardware.setKeyboardVisibility(status);
|
||||
switch (status) {
|
||||
case "OFF":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(true);
|
||||
break;
|
||||
case "ON":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
WEBVIEW.window.maximize();
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.subscribe(`${root}/set`);
|
||||
updateKeyboard(client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the keyboard visibility via the mqtt connection.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const updateKeyboard = (client) => {
|
||||
const visibility = hardware.getKeyboardVisibility();
|
||||
const root = `${INTEGRATION.discovery}/switch/${INTEGRATION.node}/keyboard`;
|
||||
if (visibility !== null) {
|
||||
client.publish(`${root}/status`, `${visibility}`, { qos: 1, retain: true });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the page number and handles the execute logic.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initPageNumber = (client) => {
|
||||
const root = `${INTEGRATION.discovery}/number/${INTEGRATION.node}/page_number`;
|
||||
const config = {
|
||||
name: "View",
|
||||
unique_id: `${INTEGRATION.node}_page_number`,
|
||||
command_topic: `${root}/set`,
|
||||
state_topic: `${root}/status`,
|
||||
value_template: "{{ value }}",
|
||||
mode: "box",
|
||||
min: 1,
|
||||
max: WEBVIEW.viewUrls.length - 1,
|
||||
unit_of_measurement: "Page",
|
||||
icon: "mdi:page-next",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client
|
||||
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === `${root}/set`) {
|
||||
const number = parseInt(message, 10);
|
||||
console.log("Set Page Number:", number);
|
||||
WEBVIEW.viewActive = number;
|
||||
}
|
||||
})
|
||||
.subscribe(`${root}/set`);
|
||||
updatePageNumber(client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the page number via the mqtt connection.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const updatePageNumber = (client) => {
|
||||
const pageNumber = WEBVIEW.viewActive;
|
||||
const root = `${INTEGRATION.discovery}/number/${INTEGRATION.node}/page_number`;
|
||||
if (pageNumber !== null) {
|
||||
client.publish(`${root}/status`, `${pageNumber}`, { qos: 1, retain: true });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the page url and handles the execute logic.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initPageUrl = (client) => {
|
||||
const root = `${INTEGRATION.discovery}/text/${INTEGRATION.node}/page_url`;
|
||||
const config = {
|
||||
name: "Url",
|
||||
unique_id: `${INTEGRATION.node}_page_url`,
|
||||
command_topic: `${root}/set`,
|
||||
state_topic: `${root}/status`,
|
||||
value_template: "{{ value }}",
|
||||
pattern: "https?://.*",
|
||||
icon: "mdi:web",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client
|
||||
.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true })
|
||||
.on("message", (topic, message) => {
|
||||
if (topic === `${root}/set`) {
|
||||
const url = message.toString();
|
||||
console.log("Set Page Url:", url);
|
||||
WEBVIEW.views[WEBVIEW.viewActive].webContents.loadURL(url);
|
||||
}
|
||||
})
|
||||
.subscribe(`${root}/set`);
|
||||
updatePageUrl(client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the page url via the mqtt connection.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const updatePageUrl = (client) => {
|
||||
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
||||
const currentUrl = WEBVIEW.views[WEBVIEW.viewActive].webContents.getURL();
|
||||
const pageUrl = !currentUrl || currentUrl.startsWith("data:") ? defaultUrl : currentUrl;
|
||||
const root = `${INTEGRATION.discovery}/text/${INTEGRATION.node}/page_url`;
|
||||
if (pageUrl !== null) {
|
||||
client.publish(`${root}/status`, `${pageUrl}`, { qos: 1, retain: true });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the model sensor.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initModel = (client) => {
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/model`;
|
||||
const config = {
|
||||
name: "Model",
|
||||
unique_id: `${INTEGRATION.node}_model`,
|
||||
state_topic: `${root}/status`,
|
||||
value_template: "{{ value }}",
|
||||
icon: "mdi:raspberry-pi",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||
updateModel(client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the model sensor via the mqtt connection.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const updateModel = (client) => {
|
||||
const model = hardware.getModel();
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/model`;
|
||||
if (model !== null) {
|
||||
client.publish(`${root}/status`, `${model}`, { qos: 1, retain: true });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the serial number sensor.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initSerialNumber = (client) => {
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/serial_number`;
|
||||
const config = {
|
||||
name: "Serial Number",
|
||||
unique_id: `${INTEGRATION.node}_serial_number`,
|
||||
state_topic: `${root}/status`,
|
||||
value_template: "{{ value }}",
|
||||
icon: "mdi:hexadecimal",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||
updateSerialNumber(client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the serial number sensor via the mqtt connection.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const updateSerialNumber = (client) => {
|
||||
const serialNumber = hardware.getSerialNumber();
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/serial_number`;
|
||||
if (serialNumber !== null) {
|
||||
client.publish(`${root}/status`, `${serialNumber}`, { qos: 1, retain: true });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the host name sensor.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initHostName = (client) => {
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/host_name`;
|
||||
const config = {
|
||||
name: "Host Name",
|
||||
unique_id: `${INTEGRATION.node}_host_name`,
|
||||
state_topic: `${root}/status`,
|
||||
value_template: "{{ value }}",
|
||||
icon: "mdi:console-network",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||
updateHostName(client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the host name sensor via the mqtt connection.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const updateHostName = (client) => {
|
||||
const hostName = hardware.getHostName();
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/host_name`;
|
||||
if (hostName !== null) {
|
||||
client.publish(`${root}/status`, `${hostName}`, { qos: 1, retain: true });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the up time sensor.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initUpTime = (client) => {
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/up_time`;
|
||||
const config = {
|
||||
name: "Up Time",
|
||||
unique_id: `${INTEGRATION.node}_up_time`,
|
||||
state_topic: `${root}/status`,
|
||||
value_template: "{{ (value | float) | round(0) }}",
|
||||
unit_of_measurement: "min",
|
||||
icon: "mdi:timeline-clock",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||
updateUpTime(client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the up time sensor via the mqtt connection.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const updateUpTime = (client) => {
|
||||
const upTime = hardware.getUpTime();
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/up_time`;
|
||||
if (upTime !== null) {
|
||||
client.publish(`${root}/status`, `${upTime}`, { qos: 1, retain: true });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the memory size sensor.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initMemorySize = (client) => {
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_size`;
|
||||
const config = {
|
||||
name: "Memory Size",
|
||||
unique_id: `${INTEGRATION.node}_memory_size`,
|
||||
state_topic: `${root}/status`,
|
||||
value_template: "{{ (value | float) | round(2) }}",
|
||||
unit_of_measurement: "GiB",
|
||||
icon: "mdi:memory",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||
updateMemorySize(client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the memory size sensor via the mqtt connection.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const updateMemorySize = (client) => {
|
||||
const memorySize = hardware.getMemorySize();
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_size`;
|
||||
if (memorySize !== null) {
|
||||
client.publish(`${root}/status`, `${memorySize}`, { qos: 1, retain: true });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the memory usage sensor.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initMemoryUsage = (client) => {
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_usage`;
|
||||
const config = {
|
||||
name: "Memory Usage",
|
||||
unique_id: `${INTEGRATION.node}_memory_usage`,
|
||||
state_topic: `${root}/status`,
|
||||
value_template: "{{ (value | float) | round(0) }}",
|
||||
unit_of_measurement: "%",
|
||||
icon: "mdi:memory-arrow-down",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||
updateMemoryUsage(client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the memory usage sensor via the mqtt connection.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const updateMemoryUsage = (client) => {
|
||||
const memoryUsage = hardware.getMemoryUsage();
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/memory_usage`;
|
||||
if (memoryUsage !== null) {
|
||||
client.publish(`${root}/status`, `${memoryUsage}`, { qos: 1, retain: true });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the processor usage sensor.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initProcessorUsage = (client) => {
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_usage`;
|
||||
const config = {
|
||||
name: "Processor Usage",
|
||||
unique_id: `${INTEGRATION.node}_processor_usage`,
|
||||
state_topic: `${root}/status`,
|
||||
value_template: "{{ (value | float) | round(0) }}",
|
||||
unit_of_measurement: "%",
|
||||
icon: "mdi:cpu-64-bit",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||
updateProcessorUsage(client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the processor usage sensor via the mqtt connection.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const updateProcessorUsage = (client) => {
|
||||
const processorUsage = hardware.getProcessorUsage();
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_usage`;
|
||||
if (processorUsage !== null) {
|
||||
client.publish(`${root}/status`, `${processorUsage}`, { qos: 1, retain: true });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the processor temperature sensor.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initProcessorTemperature = (client) => {
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_temperature`;
|
||||
const config = {
|
||||
name: "Processor Temperature",
|
||||
unique_id: `${INTEGRATION.node}_processor_temperature`,
|
||||
state_topic: `${root}/status`,
|
||||
value_template: "{{ (value | float) | round(0) }}",
|
||||
unit_of_measurement: "°C",
|
||||
icon: "mdi:radiator",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||
updateProcessorTemperature(client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the processor temperature sensor via the mqtt connection.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const updateProcessorTemperature = (client) => {
|
||||
const processorTemperature = hardware.getProcessorTemperature();
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/processor_temperature`;
|
||||
if (processorTemperature !== null) {
|
||||
client.publish(`${root}/status`, `${processorTemperature}`, { qos: 1, retain: true });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the package upgrades sensor.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initPackageUpgrades = (client) => {
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/package_upgrades`;
|
||||
const config = {
|
||||
name: "Package Upgrades",
|
||||
unique_id: `${INTEGRATION.node}_package_upgrades`,
|
||||
state_topic: `${root}/status`,
|
||||
json_attributes_topic: `${root}/attributes`,
|
||||
value_template: "{{ value }}",
|
||||
icon: "mdi:package-down",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||
updatePackageUpgrades(client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the package upgrades sensor via the mqtt connection.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const updatePackageUpgrades = (client) => {
|
||||
const packages = hardware.checkPackageUpgrades();
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/package_upgrades`;
|
||||
if (packages !== null) {
|
||||
const attributes = {
|
||||
total: packages.length,
|
||||
packages: packages.map((pkg) => pkg.replace(/\s*\[.*?\]\s*/g, "").trim()),
|
||||
};
|
||||
client.publish(`${root}/status`, `${attributes.total}`, { qos: 1, retain: true });
|
||||
client.publish(`${root}/attributes`, JSON.stringify(attributes), { qos: 1, retain: true });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the heartbeat sensor.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initHeartbeat = (client) => {
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/heartbeat`;
|
||||
const config = {
|
||||
name: "Heartbeat",
|
||||
unique_id: `${INTEGRATION.node}_heartbeat`,
|
||||
state_topic: `${root}/status`,
|
||||
value_template: "{{ value }}",
|
||||
icon: "mdi:heart-flash",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||
updateHeartbeat(client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the heartbeat sensor via the mqtt connection.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const updateHeartbeat = (client) => {
|
||||
const now = new Date();
|
||||
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60 * 1000);
|
||||
const heartbeat = local.toISOString().replace(/\.\d{3}Z$/, "");
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/heartbeat`;
|
||||
client.publish(`${root}/status`, `${heartbeat}`, { qos: 1, retain: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the last active sensor.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const initLastActive = (client) => {
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/last_active`;
|
||||
const config = {
|
||||
name: "Last Active",
|
||||
unique_id: `${INTEGRATION.node}_last_active`,
|
||||
state_topic: `${root}/status`,
|
||||
value_template: "{{ (value | float) | round(0) }}",
|
||||
unit_of_measurement: "min",
|
||||
icon: "mdi:gesture-tap-hold",
|
||||
device: INTEGRATION.device,
|
||||
};
|
||||
client.publish(`${root}/config`, JSON.stringify(config), { qos: 1, retain: true });
|
||||
updateLastActive(client);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the last active sensor via the mqtt connection.
|
||||
*
|
||||
* @param {Object} client - Instance of the mqtt client.
|
||||
*/
|
||||
const updateLastActive = (client) => {
|
||||
const now = new Date();
|
||||
const then = WEBVIEW.pointer.time;
|
||||
const lastActive = Math.abs(now - then) / (1000 * 60);
|
||||
const root = `${INTEGRATION.discovery}/sensor/${INTEGRATION.node}/last_active`;
|
||||
client.publish(`${root}/status`, `${lastActive}`, { qos: 1, retain: true });
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
init,
|
||||
update,
|
||||
};
|
||||
+696
@@ -0,0 +1,696 @@
|
||||
const path = require("path");
|
||||
const hardware = require("./hardware");
|
||||
const integration = require("./integration");
|
||||
const { app, screen, nativeTheme, ipcMain, BaseWindow, WebContentsView } = require("electron");
|
||||
|
||||
global.WEBVIEW = global.WEBVIEW || {
|
||||
initialized: false,
|
||||
status: null,
|
||||
locked: false,
|
||||
pointer: {
|
||||
position: {},
|
||||
time: new Date(),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the webview with the provided arguments.
|
||||
*
|
||||
* @returns {bool} Returns true if the initialization was successful.
|
||||
*/
|
||||
const init = async () => {
|
||||
if (ARGS.web_url.length === 0) {
|
||||
console.error("Please provide the '--web-url' parameter");
|
||||
return app.quit();
|
||||
}
|
||||
if (ARGS.web_url.some((url) => !/^https?:\/\//.test(url))) {
|
||||
console.error("Please provide the '--web-url' parameter with http(s)");
|
||||
return app.quit();
|
||||
}
|
||||
const fullscreen = ARGS.app_debug !== "true";
|
||||
const widget = ARGS.web_widget ? ARGS.web_widget === "true" : true;
|
||||
const zoom = !isNaN(parseFloat(ARGS.web_zoom)) ? parseFloat(ARGS.web_zoom) : 1.25;
|
||||
const theme = ["light", "dark"].includes(ARGS.web_theme) ? ARGS.web_theme : "dark";
|
||||
const urls = [loaderHtml(40, 1.0, theme)].concat(ARGS.web_url);
|
||||
|
||||
// Init global properties
|
||||
WEBVIEW.viewUrls = urls;
|
||||
WEBVIEW.viewZoom = zoom;
|
||||
WEBVIEW.viewTheme = theme;
|
||||
WEBVIEW.widgetTheme = theme;
|
||||
WEBVIEW.widgetEnabled = widget;
|
||||
WEBVIEW.navigationTheme = theme;
|
||||
WEBVIEW.navigationEnabled = widget;
|
||||
nativeTheme.themeSource = WEBVIEW.viewTheme;
|
||||
Object.defineProperty(WEBVIEW, "viewActive", {
|
||||
get() {
|
||||
return this._viewActive || 0;
|
||||
},
|
||||
set(v) {
|
||||
this._viewActive = Math.max(0, Math.min(v, WEBVIEW.viewUrls.length - 1));
|
||||
updateView();
|
||||
},
|
||||
});
|
||||
|
||||
// Init global root window
|
||||
WEBVIEW.window = new BaseWindow({
|
||||
title: "TouchKio",
|
||||
icon: path.join(__dirname, "..", "img", "icon.png"),
|
||||
});
|
||||
WEBVIEW.window.setMenuBarVisibility(false);
|
||||
WEBVIEW.window.setFullScreen(fullscreen);
|
||||
if (!fullscreen) {
|
||||
WEBVIEW.window.maximize();
|
||||
}
|
||||
|
||||
// Init global webview
|
||||
WEBVIEW.views = [];
|
||||
urls.forEach((url, i) => {
|
||||
const view = new WebContentsView({
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
view.setVisible(i === 0);
|
||||
view.setBackgroundColor("#FFFFFFFF");
|
||||
WEBVIEW.window.contentView.addChildView(view);
|
||||
view.webContents.loadURL(url);
|
||||
WEBVIEW.views.push(view);
|
||||
});
|
||||
|
||||
// Init global widget
|
||||
WEBVIEW.widget = new WebContentsView({
|
||||
transparent: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
},
|
||||
});
|
||||
WEBVIEW.widget.setBackgroundColor("#00000000");
|
||||
WEBVIEW.window.contentView.addChildView(WEBVIEW.widget);
|
||||
WEBVIEW.widget.webContents.loadFile(path.join(app.getAppPath(), "html", "widget.html"));
|
||||
|
||||
// Init global navigation
|
||||
WEBVIEW.navigation = new WebContentsView({
|
||||
transparent: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
},
|
||||
});
|
||||
WEBVIEW.navigation.setBackgroundColor("#00000000");
|
||||
WEBVIEW.window.contentView.addChildView(WEBVIEW.navigation);
|
||||
WEBVIEW.navigation.webContents.loadFile(path.join(app.getAppPath(), "html", "navigation.html"));
|
||||
|
||||
// Register events
|
||||
windowEvents();
|
||||
widgetEvents();
|
||||
navigationEvents();
|
||||
domEvents();
|
||||
appEvents();
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the shared webview properties.
|
||||
*/
|
||||
const update = async () => {
|
||||
if (!WEBVIEW.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update windows status
|
||||
if (WEBVIEW.window.isFullScreen()) {
|
||||
WEBVIEW.status = "Fullscreen";
|
||||
} else if (WEBVIEW.window.isMinimized()) {
|
||||
WEBVIEW.status = "Minimized";
|
||||
} else if (WEBVIEW.window.isMaximized()) {
|
||||
WEBVIEW.status = "Maximized";
|
||||
} else {
|
||||
WEBVIEW.status = "Framed";
|
||||
}
|
||||
|
||||
// Update widget status
|
||||
updateWidget();
|
||||
|
||||
// Update navigation status
|
||||
updateNavigation();
|
||||
|
||||
// Update integration sensor
|
||||
console.log("Update Kiosk Status:", WEBVIEW.status);
|
||||
integration.update();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the active view.
|
||||
*/
|
||||
const updateView = () => {
|
||||
const title = `TouchKio - ${new URL(WEBVIEW.viewUrls[WEBVIEW.viewActive]).host}`;
|
||||
console.log(`Update View: ${title} (${WEBVIEW.viewActive})`);
|
||||
WEBVIEW.window.setTitle(title);
|
||||
|
||||
// Hide all other webviews and show only the active one
|
||||
WEBVIEW.views.forEach((view, i) => {
|
||||
view.setVisible(i === WEBVIEW.viewActive);
|
||||
});
|
||||
update();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the widget control.
|
||||
*/
|
||||
const updateWidget = () => {
|
||||
// Hide keyboard button
|
||||
WEBVIEW.widget.webContents.send("button-hidden", {
|
||||
id: "keyboard",
|
||||
hidden: !HARDWARE.support.keyboardVisibility,
|
||||
});
|
||||
|
||||
// Hide navigation button
|
||||
WEBVIEW.widget.webContents.send("button-hidden", {
|
||||
id: "navigation",
|
||||
hidden: !WEBVIEW.navigationEnabled,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the navigation control.
|
||||
*/
|
||||
const updateNavigation = () => {
|
||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
||||
const currentUrl = view.webContents.getURL();
|
||||
|
||||
// Disable pager button
|
||||
WEBVIEW.navigation.webContents.send("button-disabled", {
|
||||
id: "pager",
|
||||
disabled: WEBVIEW.views.length < 3,
|
||||
});
|
||||
|
||||
// Update url text
|
||||
WEBVIEW.navigation.webContents.send("input-text", {
|
||||
id: "url",
|
||||
text: currentUrl.startsWith("data:") ? "" : currentUrl,
|
||||
placeholder: defaultUrl,
|
||||
});
|
||||
|
||||
// Disable history buttons
|
||||
WEBVIEW.navigation.webContents.send("button-disabled", {
|
||||
id: "backward",
|
||||
disabled: !view.webContents.navigationHistory.canGoBack(),
|
||||
});
|
||||
WEBVIEW.navigation.webContents.send("button-disabled", {
|
||||
id: "forward",
|
||||
disabled: !view.webContents.navigationHistory.canGoForward(),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows or hides the webview navigation bar.
|
||||
*/
|
||||
const toggleNavigation = () => {
|
||||
const window = WEBVIEW.window.getBounds();
|
||||
const navigation = WEBVIEW.navigation.getBounds();
|
||||
const height = navigation.height > 0 ? 0 : 60;
|
||||
|
||||
// Show or hide navigation based on height
|
||||
WEBVIEW.navigation.setBounds({
|
||||
x: 0,
|
||||
y: window.height - height,
|
||||
width: window.width,
|
||||
height: height,
|
||||
});
|
||||
|
||||
// Resize webview
|
||||
resizeView();
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigates backward in the history of the active webview.
|
||||
*/
|
||||
const historyBackward = () => {
|
||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||
if (view.webContents.navigationHistory.canGoBack()) {
|
||||
view.webContents.navigationHistory.goBack();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigates forward in the history of the active webview.
|
||||
*/
|
||||
const historyForward = () => {
|
||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||
if (view.webContents.navigationHistory.canGoForward()) {
|
||||
view.webContents.navigationHistory.goForward();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Activates the next webview or cycles back to the first.
|
||||
*/
|
||||
const nextView = () => {
|
||||
WEBVIEW.viewActive = (WEBVIEW.viewActive + 1) % WEBVIEW.views.length || 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reloads the current or default url on the active webview.
|
||||
*/
|
||||
const reloadView = () => {
|
||||
const view = WEBVIEW.views[WEBVIEW.viewActive];
|
||||
const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
||||
const currentUrl = view.webContents.getURL();
|
||||
|
||||
// Reload the default url or refresh the page
|
||||
if (currentUrl != defaultUrl) {
|
||||
view.webContents.loadURL(defaultUrl);
|
||||
} else {
|
||||
view.webContents.reloadIgnoringCache();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resizes and positions all webviews.
|
||||
*/
|
||||
const resizeView = () => {
|
||||
const window = WEBVIEW.window.getBounds();
|
||||
const navigation = WEBVIEW.navigation.getBounds();
|
||||
const widget = { width: 60, height: 200 };
|
||||
|
||||
// Update view size
|
||||
WEBVIEW.views.forEach((view) => {
|
||||
view.setBounds({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: window.width,
|
||||
height: window.height - navigation.height,
|
||||
});
|
||||
view.webContents.setZoomFactor(WEBVIEW.viewZoom);
|
||||
});
|
||||
|
||||
// Update widget size
|
||||
if (WEBVIEW.widgetEnabled) {
|
||||
WEBVIEW.widget.setBounds({
|
||||
x: window.width - 15,
|
||||
y: parseInt(window.height / 2 - widget.height / 2, 10),
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
});
|
||||
WEBVIEW.widget.webContents.send("data-theme", { theme: "hidden" });
|
||||
}
|
||||
|
||||
// Update navigation size
|
||||
if (WEBVIEW.navigationEnabled) {
|
||||
WEBVIEW.navigation.setBounds({
|
||||
x: 0,
|
||||
y: window.height - navigation.height,
|
||||
width: window.width,
|
||||
height: navigation.height,
|
||||
});
|
||||
WEBVIEW.navigation.webContents.send("data-theme", { theme: WEBVIEW.navigationTheme });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Register window events and handler.
|
||||
*/
|
||||
const windowEvents = () => {
|
||||
// Handle window resize events
|
||||
WEBVIEW.window.on("ready-to-show", resizeView);
|
||||
WEBVIEW.window.on("resize", resizeView);
|
||||
resizeView();
|
||||
|
||||
// Handle window focus events
|
||||
WEBVIEW.window.on("focus", () => {
|
||||
if (WEBVIEW.locked) {
|
||||
hardware.setDisplayStatus("ON");
|
||||
WEBVIEW.window.blur();
|
||||
}
|
||||
WEBVIEW.locked = false;
|
||||
});
|
||||
HARDWARE.display.notifiers.push(() => {
|
||||
WEBVIEW.locked = hardware.getDisplayStatus() === "OFF";
|
||||
if (WEBVIEW.locked) {
|
||||
WEBVIEW.window.blur();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle window status updates
|
||||
WEBVIEW.window.on("minimize", update);
|
||||
WEBVIEW.window.on("restore", update);
|
||||
WEBVIEW.window.on("maximize", update);
|
||||
WEBVIEW.window.on("unmaximize", update);
|
||||
WEBVIEW.window.on("enter-full-screen", update);
|
||||
WEBVIEW.window.on("leave-full-screen", update);
|
||||
|
||||
// Handle window touch events for activity tracking
|
||||
setInterval(() => {
|
||||
const posOld = WEBVIEW.pointer.position;
|
||||
const posNew = screen.getCursorScreenPoint();
|
||||
if (posOld.x !== posNew.x || posOld.y !== posNew.y) {
|
||||
const now = new Date();
|
||||
const then = WEBVIEW.pointer.time;
|
||||
|
||||
// Update integration sensor
|
||||
WEBVIEW.pointer.time = now;
|
||||
if (Math.abs(now - then) / 1000 > 30) {
|
||||
console.log("Update Last Active");
|
||||
integration.update();
|
||||
}
|
||||
}
|
||||
WEBVIEW.pointer.position = posNew;
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
/**
|
||||
* Register widget events and handler.
|
||||
*/
|
||||
const widgetEvents = () => {
|
||||
if (!WEBVIEW.widgetEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle widget focus events
|
||||
WEBVIEW.widget.webContents.on("focus", () => {
|
||||
const window = WEBVIEW.window.getBounds();
|
||||
const widget = WEBVIEW.widget.getBounds();
|
||||
|
||||
// Show widget
|
||||
WEBVIEW.widget.setBounds({
|
||||
x: window.width - 60,
|
||||
y: widget.y,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
});
|
||||
WEBVIEW.widget.webContents.send("data-theme", { theme: WEBVIEW.widgetTheme });
|
||||
});
|
||||
|
||||
// Handle widget blur events
|
||||
WEBVIEW.widget.webContents.on("blur", () => {
|
||||
const window = WEBVIEW.window.getBounds();
|
||||
const widget = WEBVIEW.widget.getBounds();
|
||||
|
||||
// Hide widget
|
||||
WEBVIEW.widget.setBounds({
|
||||
x: window.width - 15,
|
||||
y: widget.y,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
});
|
||||
WEBVIEW.widget.webContents.send("data-theme", { theme: "hidden" });
|
||||
});
|
||||
|
||||
// Handle widget button click events
|
||||
ipcMain.on("button-click", (e, button) => {
|
||||
switch (button.id) {
|
||||
case "keyboard":
|
||||
const toggle = hardware.getKeyboardVisibility() === "ON" ? "OFF" : "ON";
|
||||
hardware.setKeyboardVisibility(toggle);
|
||||
switch (toggle) {
|
||||
case "OFF":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(true);
|
||||
break;
|
||||
case "ON":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
WEBVIEW.window.maximize();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "fullscreen":
|
||||
if (WEBVIEW.window.isFullScreen()) {
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
} else {
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(true);
|
||||
}
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
break;
|
||||
case "minimize":
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
WEBVIEW.window.minimize();
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
break;
|
||||
case "navigation":
|
||||
toggleNavigation();
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Register navigation events and handler.
|
||||
*/
|
||||
const navigationEvents = () => {
|
||||
if (!WEBVIEW.navigationEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle navigation blur events
|
||||
let ignoreBlur = false;
|
||||
WEBVIEW.navigation.webContents.on("blur", () => {
|
||||
const visibility = hardware.getKeyboardVisibility();
|
||||
if (visibility === "ON" && !ignoreBlur) {
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.unmaximize();
|
||||
WEBVIEW.window.setFullScreen(true);
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
WEBVIEW.navigation.webContents.send("input-blur", { id: "url" });
|
||||
}
|
||||
});
|
||||
|
||||
// Handle input focus events
|
||||
ipcMain.on("input-focus", (e, input) => {
|
||||
const visibility = hardware.getKeyboardVisibility();
|
||||
switch (input.id) {
|
||||
case "url":
|
||||
if (visibility === "OFF") {
|
||||
ignoreBlur = true;
|
||||
WEBVIEW.window.restore();
|
||||
WEBVIEW.window.setFullScreen(false);
|
||||
WEBVIEW.window.maximize();
|
||||
hardware.setKeyboardVisibility("ON", () => {
|
||||
setTimeout(() => {
|
||||
WEBVIEW.navigation.webContents.focus();
|
||||
WEBVIEW.navigation.webContents.send("input-select", { id: "url" });
|
||||
ignoreBlur = false;
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle input enter events
|
||||
ipcMain.on("input-enter", (e, input) => {
|
||||
switch (input.id) {
|
||||
case "url":
|
||||
let url = input.text.trim();
|
||||
if (url && !/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(url)) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
if (!url) {
|
||||
url = WEBVIEW.viewUrls[WEBVIEW.viewActive];
|
||||
}
|
||||
WEBVIEW.views[WEBVIEW.viewActive].webContents.loadURL(url);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle navigation button click events
|
||||
ipcMain.on("button-click", (e, button) => {
|
||||
switch (button.id) {
|
||||
case "pager":
|
||||
nextView();
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
break;
|
||||
case "home":
|
||||
reloadView();
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
break;
|
||||
case "backward":
|
||||
historyBackward();
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
break;
|
||||
case "forward":
|
||||
historyForward();
|
||||
hardware.setKeyboardVisibility("OFF");
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Register dom events and handler.
|
||||
*/
|
||||
const domEvents = () => {
|
||||
WEBVIEW.views.forEach((view) => {
|
||||
// Enable webview touch emulation
|
||||
view.webContents.debugger.attach("1.1");
|
||||
view.webContents.debugger.sendCommand("Emulation.setEmitTouchEventsForMouse", {
|
||||
configuration: "mobile",
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
// Redirect webview hyperlinks
|
||||
view.webContents.setWindowOpenHandler(({ url }) => {
|
||||
view.webContents.loadURL(url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
// Remove webview scrollbar
|
||||
view.webContents.on("dom-ready", () => {
|
||||
view.webContents.insertCSS("::-webkit-scrollbar { display: none; }");
|
||||
});
|
||||
|
||||
// Webview fully loaded
|
||||
view.webContents.on("did-finish-load", () => {
|
||||
WEBVIEW.initialized = true;
|
||||
if (WEBVIEW.viewActive === 0) {
|
||||
nextView();
|
||||
}
|
||||
if (ARGS.app_debug === "true") {
|
||||
setTimeout(() => {
|
||||
view.webContents.openDevTools();
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
// Webview not loaded
|
||||
view.webContents.on("did-fail-load", (e, code, text, url, mainframe) => {
|
||||
if (mainframe) {
|
||||
if (WEBVIEW.viewActive === 0) {
|
||||
nextView();
|
||||
}
|
||||
view.webContents.loadURL(errorHtml(code, text, url));
|
||||
}
|
||||
});
|
||||
|
||||
// Webview url changed
|
||||
view.webContents.on("did-navigate-in-page", (e, url, mainframe) => {
|
||||
if (mainframe) {
|
||||
update();
|
||||
}
|
||||
});
|
||||
view.webContents.on("did-navigate", () => {
|
||||
update();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Register app events and handler.
|
||||
*/
|
||||
const appEvents = () => {
|
||||
// Handle signal and exit events
|
||||
process.on("SIGINT", app.quit);
|
||||
app.on("before-quit", () => {
|
||||
WEBVIEW.status = "Terminated";
|
||||
integration.update();
|
||||
});
|
||||
|
||||
// Handle multiple instances
|
||||
app.on("second-instance", () => {
|
||||
if (WEBVIEW.window.isMinimized()) {
|
||||
WEBVIEW.window.restore();
|
||||
}
|
||||
WEBVIEW.window.focus();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a html template for a spinning loader.
|
||||
*
|
||||
* @param {number} size - The size of the circle.
|
||||
* @param {number} speed - The rotation speed of the circle.
|
||||
* @param {string} theme - The theme used for spinner colors.
|
||||
* @returns {string} A data string with the generated html.
|
||||
*/
|
||||
const loaderHtml = (size, speed, theme) => {
|
||||
const color = {
|
||||
dark: { border: "#2A2A2A", spinner: "#03A9F4", background: "#111111" },
|
||||
light: { border: "#DCDCDC", spinner: "#03A9F4", background: "#FAFAFA" },
|
||||
}[theme];
|
||||
const html = `
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: ${color.background};
|
||||
}
|
||||
.spinner {
|
||||
width: ${size}px;
|
||||
height: ${size}px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid ${color.border};
|
||||
border-top-color: ${color.spinner};
|
||||
animation: spin ${speed}s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="spinner"></div>
|
||||
</body>
|
||||
</html>`;
|
||||
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a html template for an error page.
|
||||
*
|
||||
* @param {number} code - The error code of the response.
|
||||
* @param {string} text - The error text of the response.
|
||||
* @param {string} url - The url of the requested page.
|
||||
* @returns {string} A data string with the generated html.
|
||||
*/
|
||||
const errorHtml = (code, text, url) => {
|
||||
const html = `
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
.icon {
|
||||
margin: 0;
|
||||
font-size: 5rem;
|
||||
color: orange;
|
||||
}
|
||||
.title {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<p class="icon">⚠</p>
|
||||
<h1 class="title">Whoopsie!</h1>
|
||||
<p><strong>Loading:</strong> ${url}</p>
|
||||
<p><strong>Error:</strong> ${text} (${code})</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
init,
|
||||
update,
|
||||
};
|
||||
Reference in New Issue
Block a user