feat: additional MQTT controls and sensors #7

This commit is contained in:
leukipp
2025-01-10 17:11:11 +01:00
parent 4f426df4e6
commit fd746cc79a
3 changed files with 372 additions and 37 deletions
+174 -11
View File
@@ -19,6 +19,10 @@ global.HARDWARE = global.HARDWARE || {
}, },
notifiers: [], notifiers: [],
}, },
keyboard: {
visible: null,
notifiers: [],
},
}; };
/** /**
@@ -69,6 +73,23 @@ const init = async (args) => {
console.log(`\nDisplay Status [${HARDWARE.display.status.path}]:`, getDisplayStatus()); console.log(`\nDisplay Status [${HARDWARE.display.status.path}]:`, getDisplayStatus());
console.log(`Display Brightness [${HARDWARE.display.brightness.path}]:`, getDisplayBrightness(), "\n"); console.log(`Display Brightness [${HARDWARE.display.brightness.path}]:`, getDisplayBrightness(), "\n");
// Check for keyboard visibility changes
HARDWARE.support.keyboardVisibility = processRuns("squeekboard");
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 // Check for display changes
setInterval(update, 500); setInterval(update, 500);
@@ -124,9 +145,12 @@ const compatibleDevice = () => {
/** /**
* Queries the session type for the logged in user. * Queries the session type for the logged in user.
* *
* @returns {string} Returns session type x11/wayland or null if an error occurs. * @returns {string} Returns session type 'x11'/'wayland' or null if an error occurs.
*/ */
const sessionType = () => { const sessionType = () => {
if (!commandExists("loginctl")) {
return null;
}
return execSyncCommand("loginctl", [ return execSyncCommand("loginctl", [
"show-session", "show-session",
"$(loginctl show-user $(whoami) -p Display --value)", "$(loginctl show-user $(whoami) -p Display --value)",
@@ -146,7 +170,7 @@ const session = (type) => {
/** /**
* Gets the model name using `/sys/firmware/devicetree/base/model` or `/sys/class/dmi/id/product_name`. * Gets the model name using `/sys/firmware/devicetree/base/model` or `/sys/class/dmi/id/product_name`.
* *
* @returns {string|null} The model name of the device or "Generic" if not found. * @returns {string|null} The model name of the device or 'Generic' if not found.
*/ */
const getModel = () => { const getModel = () => {
const paths = ["/sys/firmware/devicetree/base/model", "/sys/class/dmi/id/product_name"]; const paths = ["/sys/firmware/devicetree/base/model", "/sys/class/dmi/id/product_name"];
@@ -161,7 +185,7 @@ const getModel = () => {
/** /**
* Gets the vendor name using `/sys/class/dmi/id/board_vendor`. * Gets the vendor name using `/sys/class/dmi/id/board_vendor`.
* *
* @returns {string|null} The vendor name of the device or "Generic" if not found. * @returns {string|null} The vendor name of the device or 'Generic' if not found.
*/ */
const getVendor = () => { const getVendor = () => {
const paths = ["/sys/class/dmi/id/board_vendor"]; const paths = ["/sys/class/dmi/id/board_vendor"];
@@ -180,7 +204,7 @@ const getVendor = () => {
/** /**
* Gets the serial number using `/sys/firmware/devicetree/base/serial-number` or `/sys/class/dmi/id/product_serial`. * Gets the serial number using `/sys/firmware/devicetree/base/serial-number` or `/sys/class/dmi/id/product_serial`.
* *
* @returns {string|null} The serial number of the device or "123456" if not found. * @returns {string|null} The serial number of the device or '123456' if not found.
*/ */
const getSerialNumber = () => { const getSerialNumber = () => {
const paths = ["/sys/firmware/devicetree/base/serial-number", "/sys/class/dmi/id/product_serial"]; const paths = ["/sys/firmware/devicetree/base/serial-number", "/sys/class/dmi/id/product_serial"];
@@ -195,7 +219,7 @@ const getSerialNumber = () => {
/** /**
* Gets the host machine id using `/etc/machine-id`. * Gets the host machine id using `/etc/machine-id`.
* *
* @returns {string} The machine id of the system or "123456" if not found. * @returns {string} The machine id of the system or '123456' if not found.
*/ */
const getMachineId = () => { const getMachineId = () => {
return execSyncCommand("cat", ["/etc/machine-id"]) || "123456"; return execSyncCommand("cat", ["/etc/machine-id"]) || "123456";
@@ -290,7 +314,7 @@ const getDisplayStatusPath = () => {
/** /**
* Gets the current display power status using `wlopm` or `xset`. * Gets the current display power status using `wlopm` or `xset`.
* *
* @returns {string|null} The display status ON/OFF or null if an error occurs. * @returns {string|null} The display status as 'ON'/'OFF' or null if an error occurs.
*/ */
const getDisplayStatus = () => { const getDisplayStatus = () => {
if (!HARDWARE.support.displayStatus) { if (!HARDWARE.support.displayStatus) {
@@ -323,6 +347,7 @@ const getDisplayStatus = () => {
*/ */
const setDisplayStatus = (status, callback = null) => { const setDisplayStatus = (status, callback = null) => {
if (!HARDWARE.support.displayStatus) { if (!HARDWARE.support.displayStatus) {
if (typeof callback === "function") callback(null, "Not supported");
return; return;
} }
if (status !== "ON" && status !== "OFF") { if (status !== "ON" && status !== "OFF") {
@@ -398,6 +423,7 @@ const getDisplayBrightness = () => {
*/ */
const setDisplayBrightness = (brightness, callback = null) => { const setDisplayBrightness = (brightness, callback = null) => {
if (!HARDWARE.support.displayBrightness) { if (!HARDWARE.support.displayBrightness) {
if (typeof callback === "function") callback(null, "Not supported");
return; return;
} }
if (typeof brightness !== "number" || brightness < 1 || brightness > 100) { if (typeof brightness !== "number" || brightness < 1 || brightness > 100) {
@@ -412,6 +438,52 @@ const setDisplayBrightness = (brightness, callback = null) => {
proc.stdin.end(); 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 {boolean} 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;
}
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`. * Shuts down the system using `sudo shutdown -h now`.
* *
@@ -437,14 +509,27 @@ const rebootSystem = (callback = null) => {
}; };
/** /**
* Checks if a command exists using `which`. * Checks if a process is running using `pidof`.
* *
* @param {string} cmd - The command name to check. * @param {string} name - The process name to check.
* @returns {boolean} Returns true if the command exists. * @returns {boolean} Returns true if the process runs.
*/ */
const commandExists = (cmd) => { const processRuns = (name) => {
try { try {
return !!process.execSync(`which ${cmd}`, { encoding: "utf8" }); return !!process.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 {boolean} Returns true if the command is available.
*/
const commandExists = (name) => {
try {
return !!process.execSync(`which ${name}`, { encoding: "utf8" });
} catch {} } catch {}
return false; return false;
}; };
@@ -499,6 +584,81 @@ const execAsyncCommand = (cmd, args = [], callback = null) => {
} }
} catch (error) { } catch (error) {
console.error("Execute Async:", error.message); 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 = process.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 = process.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; return proc;
@@ -524,6 +684,9 @@ module.exports = {
getDisplayBrightnessMax, getDisplayBrightnessMax,
getDisplayBrightness, getDisplayBrightness,
setDisplayBrightness, setDisplayBrightness,
getKeyboardVisibility,
setKeyboardVisibility,
checkPackageUpgrades,
shutdownSystem, shutdownSystem,
rebootSystem, rebootSystem,
}; };
+151 -6
View File
@@ -37,15 +37,15 @@ const init = async (args) => {
INTEGRATION.device = { INTEGRATION.device = {
name: `TouchKio ${deviceName}`, name: `TouchKio ${deviceName}`,
model: model, model: model,
serial_number: serialNumber,
manufacturer: vendor, manufacturer: vendor,
serial_number: serialNumber,
identifiers: [INTEGRATION.node], identifiers: [INTEGRATION.node],
sw_version: `${app.getName()}-v${app.getVersion()}`, sw_version: `${app.getName()}-v${app.getVersion()}`,
}; };
// Connection settings // Connection settings
const masked = password === null ? "null" : "*".repeat(password.length);
const options = user === null || password == null ? null : { username: user, password: password }; 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()}`); console.log("MQTT Connecting:", `${user}:${masked}@${url.toString()}`);
INTEGRATION.client = mqtt.connect(url.toString(), options); INTEGRATION.client = mqtt.connect(url.toString(), options);
@@ -60,6 +60,7 @@ const init = async (args) => {
initRefresh(INTEGRATION.client); initRefresh(INTEGRATION.client);
initKiosk(INTEGRATION.client); initKiosk(INTEGRATION.client);
initDisplay(INTEGRATION.client); initDisplay(INTEGRATION.client);
initKeyboard(INTEGRATION.client);
// Init client sensors // Init client sensors
initModel(INTEGRATION.client); initModel(INTEGRATION.client);
@@ -70,7 +71,9 @@ const init = async (args) => {
initMemoryUsage(INTEGRATION.client); initMemoryUsage(INTEGRATION.client);
initProcessorUsage(INTEGRATION.client); initProcessorUsage(INTEGRATION.client);
initProcessorTemperature(INTEGRATION.client); initProcessorTemperature(INTEGRATION.client);
initPackageUpgrades(INTEGRATION.client);
initHeartbeat(INTEGRATION.client); initHeartbeat(INTEGRATION.client);
initLastActive(INTEGRATION.client);
// Integration initialized // Integration initialized
INTEGRATION.initialized = true; INTEGRATION.initialized = true;
@@ -81,18 +84,29 @@ const init = async (args) => {
INTEGRATION.status = "offline"; INTEGRATION.status = "offline";
}); });
// Update sensor states on display change // Update sensor states from notifiers
HARDWARE.display.notifiers.push(() => { HARDWARE.display.notifiers.push(() => {
updateDisplay(INTEGRATION.client); updateDisplay(INTEGRATION.client);
}); });
HARDWARE.keyboard.notifiers.push(() => {
updateKeyboard(INTEGRATION.client);
});
// Update heartbeat time periodically (30s) // Update time sensors periodically (30s)
setInterval(() => { setInterval(() => {
updateHeartbeat(INTEGRATION.client); updateHeartbeat(INTEGRATION.client);
updateLastActive(INTEGRATION.client);
}, 30 * 1000); }, 30 * 1000);
// Update sensor states periodically (1min) // Update system sensors periodically (1min)
setInterval(update, 60 * 1000); setInterval(() => {
update();
}, 60 * 1000);
// Update package sensors periodically (60min)
setInterval(() => {
updatePackageUpgrades(INTEGRATION.client);
}, 3600 * 1000);
return true; return true;
}; };
@@ -108,6 +122,7 @@ const update = () => {
// Update client sensors // Update client sensors
updateKiosk(INTEGRATION.client); updateKiosk(INTEGRATION.client);
updateUpTime(INTEGRATION.client); updateUpTime(INTEGRATION.client);
updateLastActive(INTEGRATION.client);
updateMemoryUsage(INTEGRATION.client); updateMemoryUsage(INTEGRATION.client);
updateProcessorUsage(INTEGRATION.client); updateProcessorUsage(INTEGRATION.client);
updateProcessorTemperature(INTEGRATION.client); updateProcessorTemperature(INTEGRATION.client);
@@ -264,6 +279,9 @@ const updateKiosk = (client) => {
* @param {Object} client - Instance of the mqtt client. * @param {Object} client - Instance of the mqtt client.
*/ */
const initDisplay = (client) => { const initDisplay = (client) => {
if (!HARDWARE.support.displayStatus) {
return;
}
const root = `${INTEGRATION.discovery}/light/${INTEGRATION.node}/display`; const root = `${INTEGRATION.discovery}/light/${INTEGRATION.node}/display`;
const config = { const config = {
name: `Display`, name: `Display`,
@@ -314,6 +332,62 @@ const updateDisplay = (client) => {
} }
}; };
/**
* 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",
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 model sensor. * Initializes the model sensor.
* *
@@ -575,6 +649,44 @@ const updateProcessorTemperature = (client) => {
} }
}; };
/**
* 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. * Initializes the heartbeat sensor.
* *
@@ -607,6 +719,39 @@ const updateHeartbeat = (client) => {
client.publish(`${root}/status`, `${heartbeat}`, { qos: 1, retain: true }); 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 = { module.exports = {
init, init,
update, update,
+47 -20
View File
@@ -1,12 +1,16 @@
const path = require("path"); const path = require("path");
const hardware = require("./hardware"); const hardware = require("./hardware");
const integration = require("./integration"); const integration = require("./integration");
const { app, nativeTheme, BaseWindow, WebContentsView } = require("electron"); const { app, screen, nativeTheme, BaseWindow, WebContentsView } = require("electron");
global.WEBVIEW = global.WEBVIEW || { global.WEBVIEW = global.WEBVIEW || {
initialized: false, initialized: false,
status: "offline", status: "offline",
locked: false, locked: false,
pointer: {
position: {},
time: new Date(),
},
}; };
/** /**
@@ -40,6 +44,7 @@ const init = async (args) => {
// Register events // Register events
windowEvents(WEBVIEW.window, zoom); windowEvents(WEBVIEW.window, zoom);
touchEvents(WEBVIEW.window);
viewEvents(WEBVIEW.view); viewEvents(WEBVIEW.view);
return true; return true;
@@ -86,24 +91,6 @@ const windowEvents = (window, zoom) => {
window.on("resize", resize); window.on("resize", resize);
resize(); resize();
// Handle focus change for webview lock
HARDWARE.display.notifiers.push(() => {
WEBVIEW.locked = hardware.getDisplayStatus() === "OFF";
if (WEBVIEW.locked) {
window.blur();
}
/* This also works, but it doesn't allow to react for screen wake up.
window.setIgnoreMouseEvents(hardware.getDisplayStatus() === "OFF");
*/
});
window.on("focus", () => {
if (WEBVIEW.locked) {
hardware.setDisplayStatus("ON");
window.blur();
}
WEBVIEW.locked = false;
});
// Handle window status updates // Handle window status updates
window.on("minimize", update); window.on("minimize", update);
window.on("restore", update); window.on("restore", update);
@@ -128,6 +115,46 @@ const windowEvents = (window, zoom) => {
}); });
}; };
/**
* Register touch events and handler.
*
* @param {Object} window - The root window object.
*/
const touchEvents = (window) => {
// Handle focus change for webview lock
window.on("focus", () => {
if (WEBVIEW.locked) {
hardware.setDisplayStatus("ON");
window.blur();
}
WEBVIEW.locked = false;
});
HARDWARE.display.notifiers.push(() => {
WEBVIEW.locked = hardware.getDisplayStatus() === "OFF";
if (WEBVIEW.locked) {
window.blur();
}
});
// Handle 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;
}, 1 * 1000);
};
/** /**
* Register webview events and handler. * Register webview events and handler.
* *
@@ -137,8 +164,8 @@ const viewEvents = (view) => {
// Enable webview touch emulation // Enable webview touch emulation
view.webContents.debugger.attach("1.1"); view.webContents.debugger.attach("1.1");
view.webContents.debugger.sendCommand("Emulation.setEmitTouchEventsForMouse", { view.webContents.debugger.sendCommand("Emulation.setEmitTouchEventsForMouse", {
enabled: true,
configuration: "mobile", configuration: "mobile",
enabled: true,
}); });
// Disable webview hyperlinks // Disable webview hyperlinks