From 61628b1864e99c13c24a976ba716734ebc905672 Mon Sep 17 00:00:00 2001 From: leukipp Date: Tue, 25 Nov 2025 17:12:43 +0100 Subject: [PATCH] feat: add mqtt screenshot image #133 --- js/integration.js | 31 ++++++++++++++++++++++ js/webview.js | 65 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 89 insertions(+), 7 deletions(-) diff --git a/js/integration.js b/js/integration.js index 2206f00..d226acb 100644 --- a/js/integration.js +++ b/js/integration.js @@ -94,6 +94,7 @@ const init = async () => { initLastActive(); // Init client diagnostic + initScreenshot(); initHeartbeat(); initErrors(); initVersion(); @@ -116,6 +117,7 @@ const init = async () => { updateDisplay(); updateLastActive(); }); + EVENTS.on("updateScreenshot", updateScreenshot); EVENTS.on("consoleLog", updateErrors); }) .on("error", (error) => { @@ -452,10 +454,12 @@ const initDisplay = () => { unique_id: `${INTEGRATION.node}_display`, command_topic: `${root}/power/set`, state_topic: `${root}/power/state`, + supported_color_modes: ["onoff"], icon: "mdi:monitor-shimmer", platform: "light", device: INTEGRATION.device, ...(HARDWARE.support.displayBrightness && { + supported_color_modes: ["brightness"], brightness_command_topic: `${root}/brightness/set`, brightness_state_topic: `${root}/brightness/state`, brightness_scale: 100, @@ -1050,6 +1054,33 @@ const updateLastActive = async () => { publishAttributes("last_active", tracker); }; +/** + * Initializes the page screenshot. + */ +const initScreenshot = () => { + const root = `${INTEGRATION.root}/screenshot`; + const config = { + name: "Screenshot", + unique_id: `${INTEGRATION.node}_screenshot`, + image_topic: `${root}/state`, + image_encoding: "b64", + content_type: "image/png", + entity_category: "diagnostic", + icon: "mdi:image-area", + device: INTEGRATION.device, + }; + publishConfig("image", config); + updateScreenshot(); +}; + +/** + * Updates the page screenshot via the mqtt connection. + */ +const updateScreenshot = async () => { + const screenshot = WEBVIEW.tracker.screenshot; + publishState("screenshot", screenshot); +}; + /** * Initializes the heartbeat sensor. */ diff --git a/js/webview.js b/js/webview.js index 91c642a..1d3e87e 100644 --- a/js/webview.js +++ b/js/webview.js @@ -29,6 +29,7 @@ global.WEBVIEW = global.WEBVIEW || { widget: { focused: null, }, + screenshot: null, }, }; @@ -314,6 +315,11 @@ const updateView = () => { WEBVIEW.status.webContents.send("text-content", { id: "title", content: title }); 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 stored theme and zoom cookieStore("web-theme").then((value) => { if (value && value !== WEBVIEW.theme.get()) { @@ -326,9 +332,9 @@ const updateView = () => { } }); - // Hide all other webviews and show only the active one - WEBVIEW.views.forEach((view, i) => { - view.setVisible(i === WEBVIEW.viewActive); + // Update webview screenshot + captureView(1000).then(() => { + EVENTS.emit("updateScreenshot"); }); EVENTS.emit("updatePage"); update(); @@ -711,6 +717,11 @@ const resizeView = () => { theme: WEBVIEW.navigationTheme, }); } + + // Update webview screenshot + captureView(1000).then(() => { + EVENTS.emit("updateScreenshot"); + }); }; /** @@ -802,8 +813,8 @@ const windowEvents = async () => { historyForward(); }); - // Check for window touch events (1s) - setInterval(() => { + // Check for window touch events (every full 1s) + interval(() => { if (APP.exiting) { return; } @@ -1231,8 +1242,18 @@ const appEvents = async () => { console.error(`${details.type} Process ${details.reason} (code ${details.exitCode}): ${name}`); }); - // Check latest release (2h) - setInterval(() => { + // Update latest screenshot (every full 1min) + interval(() => { + if (APP.exiting) { + return; + } + captureView(5000).then(() => { + EVENTS.emit("updateScreenshot"); + }); + }, 60 * 1000); + + // Check latest release (every full 2h) + interval(() => { if (APP.exiting) { return; } @@ -1341,6 +1362,22 @@ const cookieStore = async (key, value, view = WEBVIEW.views[WEBVIEW.viewActive]) return !isNaN(Number(cookie.value)) ? Number(cookie.value) : cookie.value; }; +/** + * Captures a webview screenshot as a base64 image. + * + * @param {number} wait - The time to wait before capturing in milliseconds. + * @param {WebContentsView} view - The webview that captures the page. + * @returns {Promise} The base64 image of the captured page or null if failed. + */ +const captureView = async (wait, view = WEBVIEW.views[WEBVIEW.viewActive]) => { + await new Promise((r) => setTimeout(r, wait)); + const image = await view.webContents.capturePage(); + const dataUrl = image.toDataURL(); + const dataString = dataUrl.replace(/^data:image\/\w+;base64,/, "").trim(); + WEBVIEW.tracker.screenshot = dataString || WEBVIEW.tracker.screenshot; + return WEBVIEW.tracker.screenshot; +}; + /** * Generates a html template for a spinning loader. * @@ -1439,6 +1476,20 @@ const errorHtml = (code, text, url, theme) => { return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`; }; +/** + * Interval function that runs at interval boundaries. + * + * @param {Function} callback - The function to execute. + * @param {number} ms - The interval time in milliseconds. + */ +const interval = (callback, ms) => { + const delay = ms - (Date.now() % ms); + setTimeout(() => { + callback(); + setInterval(callback, ms); + }, delay); +}; + module.exports = { init, update,