UNPKG

iobroker.lovelace

Version:

With this adapter you can build visualization for ioBroker with Home Assistant Lovelace UI

1,152 lines (1,151 loc) 43.2 kB
"use strict"; const instancesPath = "instances."; const yaml = require("js-yaml"); const BROWSER_MOD_VERSION = "2.13.5"; class BrowserModModule { adapter; objects; clients; browserModStorage; knownViews; knownViewsStates; /** * Create a new instance of the browser_mod module. * * @param options - options object with adapter and objects * @param options.adapter - ioBroker adapter instance * @param options.objects - ioBroker objects cache */ constructor(options) { this.adapter = options.adapter; this.objects = options.objects; this.clients = {}; this.browserModStorage = { browsers: {}, version: BROWSER_MOD_VERSION, settings: { hideSidebar: true, hideHeader: false, defaultPanel: null, sidebarPanelOrder: null, sidebarHiddenPanels: null, sidebarTitle: null, faviconTemplate: null, titleTemplate: null, hideInteractIcon: true, autoRegister: true, lockRegister: null }, user_settings: {}, sessions: {} }; this.knownViews = []; this.knownViewsStates = {}; } /** * Check if all objects for the browser_mod integration are created. * * @param ioBrokerDeviceId - ioBroker device id (path within adapter namespace) * @param browserId - browser_mod browser id * @param battery - whether to create battery state objects */ async _checkObjects(ioBrokerDeviceId, browserId, battery = false) { ioBrokerDeviceId = `${this.adapter.namespace}.${ioBrokerDeviceId}`; if (!this.objects[ioBrokerDeviceId]) { if (!browserId) { await this.adapter.setObjectNotExistsAsync(ioBrokerDeviceId, { type: "folder", common: { name: "UI Instances" }, native: {} }); } else { await this.adapter.setObjectNotExistsAsync(ioBrokerDeviceId, { type: "device", common: { name: browserId, statusStates: { onlineId: `${ioBrokerDeviceId}.online` } }, native: { instance: browserId } }); this.adapter.log.info(`New browser_mod instance ${browserId}`); } } if (!this.objects[`${ioBrokerDeviceId}.path`]) { await this.adapter.setObjectNotExistsAsync(`${ioBrokerDeviceId}.path`, { type: "state", common: { name: "UI is showing path", type: "string", read: true, write: true, role: "state", states: this.knownViewsStates }, native: { instance: browserId } }); } if (!this.objects[`${ioBrokerDeviceId}.visible`] && browserId) { await this.adapter.setObjectNotExistsAsync(`${ioBrokerDeviceId}.visible`, { type: "state", common: { name: "UI is visible", type: "boolean", read: true, write: false, role: "sensor" }, native: { instance: browserId } }); } if (!this.objects[`${ioBrokerDeviceId}.activity`] && browserId) { await this.adapter.setObjectNotExistsAsync(`${ioBrokerDeviceId}.activity`, { type: "state", common: { name: "User is active in this browser", type: "boolean", read: true, write: false, role: "sensor" }, native: { instance: browserId } }); } if (battery) { if (!this.objects[`${ioBrokerDeviceId}.battery`] && browserId) { await this.adapter.setObjectNotExistsAsync(`${ioBrokerDeviceId}.battery`, { type: "channel", common: { name: "battery" }, native: {} }); } if (!this.objects[`${ioBrokerDeviceId}.battery.level`] && browserId) { await this.adapter.setObjectNotExistsAsync(`${ioBrokerDeviceId}.battery.level`, { type: "state", common: { name: "battery", type: "number", read: true, write: false, role: "value.battery" }, native: { instance: browserId } }); } if (!this.objects[`${ioBrokerDeviceId}.battery.charging`] && browserId) { await this.adapter.setObjectNotExistsAsync(`${ioBrokerDeviceId}.battery.charging`, { type: "state", common: { name: "charging", type: "boolean", read: true, write: false, role: "indicator.plugged" }, native: { instance: browserId } }); } } if (this.objects[`${ioBrokerDeviceId}.name`]) { await this.adapter.delObjectAsync(`${ioBrokerDeviceId}.name`); } if (!this.objects[`${ioBrokerDeviceId}.more_info`]) { await this.adapter.setObjectNotExistsAsync(`${ioBrokerDeviceId}.more_info`, { type: "state", common: { name: "Show more_info of entity_id", type: "string", read: false, write: true, role: "state" }, native: { instance: browserId } }); } if (!this.objects[`${ioBrokerDeviceId}.toast`]) { await this.adapter.setObjectNotExistsAsync(`${ioBrokerDeviceId}.toast`, { type: "state", common: { name: { en: "Notification in bottom left.", de: "Benachrichtigung unten links" }, desc: { en: "Simple text or optional as json with fields: message, duration, action_text, action, see browser_mod notification dokumentation", de: "Einfacher text oder optional als json mit den Feldern: message, duration, action_text, action, wie in der Dokumentation zu browser_mod unter dem Punkt notification" }, type: "string", read: false, write: true, role: "json" }, native: { instance: browserId } }); } if (this.objects[`${ioBrokerDeviceId}.notification`]) { await this.adapter.delObjectAsync(`${ioBrokerDeviceId}.notification`); } if (!this.objects[`${ioBrokerDeviceId}.popup`]) { await this.adapter.setObjectNotExistsAsync(`${ioBrokerDeviceId}.popup`, { type: "state", common: { name: "Show popup", type: "string", read: false, write: true, role: "json" }, native: { instance: browserId } }); } if (!this.objects[`${ioBrokerDeviceId}.popup_close`]) { await this.adapter.setObjectNotExistsAsync(`${ioBrokerDeviceId}.popup_close`, { type: "state", common: { name: "Close popups or more_info dialogs.", type: "boolean", read: false, write: true, role: "button" }, native: { instance: browserId } }); } if (!this.objects[`${ioBrokerDeviceId}.refresh`]) { await this.adapter.setObjectNotExistsAsync(`${ioBrokerDeviceId}.refresh`, { type: "state", common: { name: "Reload webpage", type: "boolean", read: false, write: true, role: "button" }, native: { instance: browserId } }); } if (this.objects[`${ioBrokerDeviceId}.window_reload`]) { await this.adapter.delObjectAsync(`${ioBrokerDeviceId}.window_reload`); } if (this.objects[`${ioBrokerDeviceId}.lovelace_reload`]) { await this.adapter.delObjectAsync(`${ioBrokerDeviceId}.lovelace_reload`); } if (!this.objects[`${ioBrokerDeviceId}.blackout`]) { await this.adapter.setObjectNotExistsAsync(`${ioBrokerDeviceId}.blackout`, { type: "state", common: { name: "Blackout screen", type: "boolean", read: false, write: true, role: "button" }, native: { instance: browserId } }); } if (!this.objects[`${ioBrokerDeviceId}.set_theme`]) { await this.adapter.setObjectNotExistsAsync(`${ioBrokerDeviceId}.set_theme`, { type: "state", common: { name: { en: "Set frontend theme", de: "Frontend-Theme setzen" }, desc: { en: 'Theme name (see dropdown). Advanced: JSON with fields theme, dark ("auto"/"light"/"dark"), primaryColor.', de: 'Theme-Name (siehe Auswahl). Erweitert: JSON mit Feldern theme, dark ("auto"/"light"/"dark"), primaryColor.' }, type: "string", read: false, write: true, role: "text", states: this._getThemeStates() }, native: { instance: browserId } }); } if (!this.objects[`${ioBrokerDeviceId}.console`]) { await this.adapter.setObjectNotExistsAsync(`${ioBrokerDeviceId}.console`, { type: "state", common: { name: { en: "Log message to browser console", de: "Nachricht in Browser-Konsole ausgeben" }, type: "string", read: false, write: true, role: "text" }, native: { instance: browserId } }); } if (!this.objects[`${ioBrokerDeviceId}.change_browser_id`] && browserId) { await this.adapter.setObjectNotExistsAsync(`${ioBrokerDeviceId}.change_browser_id`, { type: "state", common: { name: { en: "Change this Browser ID", de: "Diese Browser-ID \xE4ndern" }, desc: { en: "New Browser ID as plain text, or JSON with fields: new_browser_id, register (bool), refresh (bool).", de: "Neue Browser-ID als Text, oder JSON mit Feldern: new_browser_id, register (bool), refresh (bool)." }, type: "string", read: true, write: true, role: "text" }, native: { instance: browserId } }); await this.adapter.setStateAsync(`${ioBrokerDeviceId}.change_browser_id`, browserId, true); } if (!this.objects[`${ioBrokerDeviceId}.online`] && browserId) { await this.adapter.setObjectNotExistsAsync(`${ioBrokerDeviceId}.online`, { type: "state", common: { name: "online", type: "boolean", read: true, write: false, role: "indicator.reachable", def: true }, native: { instance: browserId } }); } await this._checkSettingState(ioBrokerDeviceId, browserId, "hideHeader", "Hide Header"); await this._checkSettingState(ioBrokerDeviceId, browserId, "hideSidebar", "Hide Sidebar"); } /** * Create (and on the root level, seed) the hideHeader / hideSidebar switch state, or - if it * already exists - read its value into the browser_mod storage. The root object (no browserId, * `instances.<key>`) is the "target all" default: its `def` and seeded value come from the global * default and are read back on init as the default for new browsers; per-browser objects mirror * that default. * * @param ioBrokerDeviceId - fully namespaced device id (root `instances` or `instances.<browserId>`) * @param browserId - browser id, or undefined for the root "target all" object * @param key - which setting (`hideHeader` or `hideSidebar`) * @param name - display name for the object */ async _checkSettingState(ioBrokerDeviceId, browserId, key, name) { const stateId = `${ioBrokerDeviceId}.${key}`; if (!this.objects[stateId]) { const def = !!this.browserModStorage.settings[key]; await this.adapter.setObjectNotExistsAsync(stateId, { type: "state", common: { name, type: "boolean", read: true, write: true, role: "switch", def }, native: { instance: browserId } }); if (!browserId) { await this.adapter.setStateAsync(stateId, def, true); } } else { const settingState = await this.adapter.getStateAsync(stateId); if (settingState) { if (browserId) { this.initialiseBrowserSettings(browserId, true); this.browserModStorage.browsers[browserId].settings[key] = settingState.val; } else { this.browserModStorage.settings[key] = settingState.val; } } } } /** * Clean up old browser_mod instances. */ async _cleanUpInstances() { const count = Object.keys(this.browserModStorage.browsers).length; if (count > this.adapter.config.maxBrowserInstances) { this.adapter.log.info( `Cleaning up ${count - this.adapter.config.maxBrowserInstances} old browser_mod instances.` ); const browsersSorted = Object.keys(this.browserModStorage.browsers).sort( (a, b) => this.browserModStorage.browsers[a].last_seen - this.browserModStorage.browsers[b].last_seen ); for (let i = 0; i < count - this.adapter.config.maxBrowserInstances; i += 1) { const browserId = browsersSorted[i]; this.adapter.log.debug( `Deleting old browser_mod instance ${browserId}, last seen ${new Date(this.browserModStorage.browsers[browserId].last_seen).toISOString()}` ); await this.adapter.delObjectAsync(`${instancesPath}${browserId}`, { recursive: true }); delete this.browserModStorage.browsers[browserId]; } } } /** * Initialise the browser settings for a browser_mod instance. * * @param browserId - browser_mod browser id * @param now - whether to set last_seen to current time */ initialiseBrowserSettings(browserId, now = false) { if (!this.browserModStorage.browsers[browserId]) { this.browserModStorage.browsers[browserId] = { last_seen: now ? Date.now() : 0, registered: true, locked: false, camera: false, // Copy, not a reference: a shared object would let per-browser hideSidebar/hideHeader // writes mutate the global defaults and every other browser's settings. settings: { ...this.browserModStorage.settings }, meta: "default" }; } } /** * Handle an update message from a browser_mod instance. * * @param ioBrokerDeviceId - ioBroker device id path for the browser instance * @param message - the update message from the browser */ async _handleUpdate(ioBrokerDeviceId, message) { if (message.browserID && this.browserModStorage.browsers[message.browserID]) { this.browserModStorage.browsers[message.browserID].last_seen = Date.now() / 1e3; } const data = message.data; if (data) { if (data.browser) { const browser = data.browser; if (browser.battery_level) { await this._checkObjects(ioBrokerDeviceId, message.browserID, true); await this.adapter.setState( `${ioBrokerDeviceId}.battery.level`, browser.battery_level, true ); await this.adapter.setState( `${ioBrokerDeviceId}.battery.charging`, browser.charging || false, true ); } else { await this._checkObjects(ioBrokerDeviceId, message.browserID); await this._cleanUpInstances(); } if (browser.path) { await this.adapter.setState(`${ioBrokerDeviceId}.path`, browser.path, true); } if (browser.visibility) { await this.adapter.setState(`${ioBrokerDeviceId}.visible`, browser.visibility === "visible", true); } } if (typeof data.activity === "boolean") { await this.adapter.setState(`${ioBrokerDeviceId}.activity`, data.activity, true); } if (typeof data.screen_on === "boolean") { await this.adapter.setState(`${ioBrokerDeviceId}.blackout`, !data.screen_on, true); } } } /** * Send a message to a browser_mod instance. * * @param client - the browser_mod client to send to * @param message - message payload to send */ _sendToClient(client, message) { client.ws.send( JSON.stringify({ id: client.subscribeId, ...message }) ); } /** * (Re-)send the browser_mod "ready" event (the full storage incl. this browser's settings) to a * live client. Sent again after a (re)register so the card re-applies settings like hideSidebar * once it is registered and the sidebar element exists - otherwise the very first connect can apply * hideSidebar before the sidebar is ready and the user has to press F5. * * @param client - the browser_mod client to notify */ _sendReadyEvent(client) { this._sendToClient(client, { type: "event", event: { event_type: "ready", origin: "LOCAL", result: this.browserModStorage, time_fired: (/* @__PURE__ */ new Date()).toISOString() } }); } /** * Apply a root "target all" setting change (hideHeader/hideSidebar) to every known browser: update * its in-memory setting and its per-instance mirror state. The per-instance setState uses ack=true * so it does not re-trigger onStateChange. * * @param key - which setting (`hideHeader` or `hideSidebar`) * @param val - the new value */ async _applyRootSettingToAll(key, val) { for (const browserId of Object.keys(this.browserModStorage.browsers)) { this.initialiseBrowserSettings(browserId); this.browserModStorage.browsers[browserId].settings[key] = val; const stateId = `${instancesPath}${browserId}.${key}`; if (this.objects[`${this.adapter.namespace}.${stateId}`]) { await this.adapter.setStateAsync(stateId, val, true); } } } /** * Write the per-browser setting VALUES onto their mirror states. _checkObjects only seeds the * objects with the global default in common.def; it never writes the state value. Used on * (re)register and rename so the ioBroker states reflect the browser's actual settings. * * @param ioBrokerDeviceId - the browser's device path (e.g. instances.<id>) * @param settings - the browser's settings, if known */ async _applySettingStates(ioBrokerDeviceId, settings) { if (!settings) { return; } for (const key of ["hideSidebar", "hideHeader"]) { if (settings[key] !== void 0) { await this.adapter.setState(`${ioBrokerDeviceId}.${key}`, !!settings[key], true); } } } /** * Derive a stable key for the current login session, used for sync-session Browser ID recall. * Prefers the auth token, falls back to the username. Returns undefined when neither is known. * * @param ws - websocket connection */ _sessionKey(ws) { var _a, _b; return ((_a = ws.__auth) == null ? void 0 : _a.access_token) || ((_b = ws.__auth) == null ? void 0 : _b.username) || void 0; } /** * Build the `common.states` value→label map of theme names available for set_theme. * Parses the same theme YAML the server uses, plus the built-in 'default'/'auto' entries. */ _getThemeStates() { const states = { default: "default", auto: "auto" }; try { const themes = yaml.load(this.adapter.config.themes || "") || {}; for (const themeName of Object.keys(themes)) { states[themeName] = themeName; } } catch (e) { this.adapter.log.debug(`Could not parse themes for set_theme states: ${String(e)}`); } return states; } /** * Handle a `browser_mod.*` service call from the frontend (e.g. browser_mod.notification, * browser_mod.refresh). Translates it into a browser command and forwards it to the target * browser(s). Without this, such calls fell through to the generic entity handler and produced * a misleading "Unknown entity" warning. * * @param ws - websocket connection the call came in on * @param data - the call_service payload * @returns true if handled */ processServiceCall(ws, data) { if (data.domain !== "browser_mod") { return false; } const service = data.service; const serviceData = { ...data.service_data || {} }; if (service === "notification" && typeof serviceData.message === "string" && serviceData.message.includes("version mismatch")) { this.adapter.log.warn( `browser_mod reports: "${serviceData.message}". The browser_mod frontend ships with ioBroker.lovelace (expected version ${BROWSER_MOD_VERSION}). Do NOT install your own browser_mod - remove it so the bundled version is used.` ); } let browserId = serviceData.browser_id; if (browserId === "THIS") { browserId = ws.browserID; } delete serviceData.browser_id; const event = { event_type: "browser_mod/command", command: service, origin: "LOCAL", time_fired: (/* @__PURE__ */ new Date()).toISOString(), ...serviceData }; if (browserId && this.clients[browserId]) { const client = this.clients[browserId]; this._sendToClient(client, { type: "event", event: { ...event, browserID: client.instance } }); } else { for (const client of Object.values(this.clients)) { this._sendToClient(client, { type: "event", event: { ...event, browserID: client.instance } }); } } ws.send(JSON.stringify({ id: data.id, type: "result", success: true, result: { context: { id: null } } })); return true; } /** * Process a message from a browser_mod instance. * * @param ws - websocket connection with browser id * @param message - the message from the frontend */ async processMessage(ws, message) { var _a; if (message.type && message.type.startsWith("browser_mod/")) { const method = message.type.split("/")[1]; if (!message.browserID && method !== "recall_id") { this.adapter.log.warn(`No browser ID in browser_mod request: ${JSON.stringify(message)}`); return true; } const ioBrokerDeviceId = instancesPath + message.browserID; if (method === "update") { await this._handleUpdate(ioBrokerDeviceId, message); } else if (method === "connect") { ws.on("close", async () => { const currentId = ws.browserID || message.browserID; this.adapter.log.debug(`Instance ${currentId} disconnected.`); delete this.clients[currentId]; if (this.objects[`${this.adapter.namespace}.${instancesPath}${currentId}.online`]) { await this.adapter.setStateAsync(`${instancesPath}${currentId}.online`, false, true); } }); this.adapter.log.debug(`Instance ${String(message.browserID)} connected.`); this.clients[message.browserID] = { subscribeId: message.id, instance: message.browserID, ws }; ws.browserID = message.browserID; ws.send( JSON.stringify([ { id: message.id, type: "result", success: true, result: null }, { id: message.id, type: "event", event: { event_type: "ready", origin: "LOCAL", result: this.browserModStorage, time_fired: (/* @__PURE__ */ new Date()).toISOString() } } ]) ); if (this.objects[`${this.adapter.namespace}.${ioBrokerDeviceId}.online`]) { await this.adapter.setStateAsync(`${ioBrokerDeviceId}.online`, true, true); } else { this.adapter.log.debug(`No objects for instance, yet.. ${ioBrokerDeviceId}.online`); } } else if (method === "register") { this.initialiseBrowserSettings(message.browserID, true); const msgData = message.data; if (msgData && msgData.browserID) { const oldBrowserId = message.browserID; const newBrowserId = msgData.browserID; const newIoBrokerDeviceId = instancesPath + newBrowserId; const oldPrefix = `${this.adapter.namespace}.${ioBrokerDeviceId}`; for (const id of Object.keys(this.objects)) { if (id.startsWith(oldPrefix)) { delete this.objects[id]; } } try { await this.adapter.delObjectAsync(ioBrokerDeviceId, { recursive: true }); await this._checkObjects(newIoBrokerDeviceId, newBrowserId); } catch (e) { this.adapter.log.warn( `Could not delete old instance objects in ${ioBrokerDeviceId}, please do so yourself. Error was: ${String(e)}` ); } delete this.browserModStorage.browsers[oldBrowserId]; delete msgData.browserID; this.browserModStorage.browsers[newBrowserId] = msgData; await this._applySettingStates( newIoBrokerDeviceId, msgData.settings ); const liveClient = this.clients[oldBrowserId]; if (liveClient) { delete this.clients[oldBrowserId]; liveClient.instance = newBrowserId; liveClient.ws.browserID = newBrowserId; this.clients[newBrowserId] = liveClient; } if (this.clients[newBrowserId]) { await this.adapter.setStateAsync(`${newIoBrokerDeviceId}.online`, true, true); this._sendReadyEvent(this.clients[newBrowserId]); } this.adapter.log.info(`browser_mod instance renamed: ${oldBrowserId} -> ${newBrowserId}`); } else { try { await this._checkObjects(ioBrokerDeviceId, message.browserID); await this._cleanUpInstances(); await this._applySettingStates( ioBrokerDeviceId, msgData == null ? void 0 : msgData.settings ); } catch (e) { this.adapter.log.warn( `Could not create objects for instance ${ioBrokerDeviceId}. Error was: ${String(e)}` ); } if (this.objects[`${this.adapter.namespace}.${ioBrokerDeviceId}.online`]) { await this.adapter.setStateAsync(`${ioBrokerDeviceId}.online`, true, true); } else { this.adapter.log.debug(`No objects for instance, yet.. ${ioBrokerDeviceId}.online`); } const client = this.clients[message.browserID]; if (client) { this._sendReadyEvent(client); } } } else if (method === "log") { this.adapter.log.debug(`Message from browser_mod: ${String(message.message)}`); ws.send(JSON.stringify({ id: message.id, type: "result", success: true })); } else if (method === "settings") { if (message.key) { if (message.user) { const user = message.user; const userSettings = this.browserModStorage.user_settings[user] || (this.browserModStorage.user_settings[user] = {}); userSettings[message.key] = message.value; } else { this.browserModStorage.settings[message.key] = message.value; } this.adapter.log.debug( `Updated browser_mod settings: ${message.key} to ${String(message.value)}` ); } ws.send(JSON.stringify({ id: message.id, type: "result", success: true })); } else if (method === "store_session") { const sessionKey = this._sessionKey(ws); if (sessionKey && message.browserID) { this.browserModStorage.sessions[sessionKey] = message.browserID; } ws.send(JSON.stringify({ id: message.id, type: "result", success: true })); } else if (method === "delete_session") { const sessionKey = this._sessionKey(ws); if (sessionKey) { delete this.browserModStorage.sessions[sessionKey]; } ws.send(JSON.stringify({ id: message.id, type: "result", success: true })); } else if (method === "recall_id") { const sessionKey = this._sessionKey(ws); const sessionBrowserId = sessionKey ? this.browserModStorage.sessions[sessionKey] : void 0; const result = sessionBrowserId ? { browserID: sessionBrowserId, via_session: true } : { browserID: (_a = ws.browserID) != null ? _a : null }; ws.send(JSON.stringify({ id: message.id, type: "result", success: true, result })); } else if (method === "unregister") { const browserId = message.browserID; try { await this.adapter.delObjectAsync(`${instancesPath}${browserId}`, { recursive: true }); } catch (e) { this.adapter.log.info(`Could not delete browser_mod instance ${browserId} objects: ${String(e)}`); this.adapter.log.info("Maybe was already deleted?"); } delete this.browserModStorage.browsers[browserId]; this.adapter.log.debug(`Instance ${browserId} unregistered.`); ws.send(JSON.stringify({ id: message.id, type: "result", success: true })); } else { this.adapter.log.warn(`Unknown browser_mod method: ${JSON.stringify(message)}`); ws.send(JSON.stringify({ id: message.id, type: "result", success: true })); } return true; } else { return false; } } /** * Handle a state change in ioBroker. * * @param id - ioBroker state id that changed * @param state - the new state value */ onStateChange(id, state) { if (state && !state.ack && id.startsWith(`${this.adapter.namespace}.${instancesPath}`)) { const parts = id.split("."); const browserId = parts[3]; let command = parts[4]; let allDevices = false; if (!command) { command = parts[3]; allDevices = true; } let event = { event_type: "browser_mod/command", command, origin: "LOCAL", time_fired: (/* @__PURE__ */ new Date()).toISOString() }; const client = this.clients[browserId]; if (allDevices || client) { switch (command) { case "blackout": if (!state.val) { event.command = "screen_on"; } else { event.command = "screen_off"; } break; case "path": event.command = "navigate"; event.path = state.val; break; case "more_info": event.entity = state.val; break; case "toast": event.command = "notification"; if (state.val) { try { const { duration, message, action_text, action } = JSON.parse( state.val ); event.duration = duration; event.message = message; event.action_text = action_text; event.action = action; } catch (e) { this.adapter.log.error(`Could not parse toast object: ${String(e)}`); const valStr = state.val; if (valStr.includes(";")) { [event.duration, event.message, event.action_text, event.action] = valStr.split(";"); if (event.action) { try { event.action = JSON.parse(event.action); } catch (e2) { this.adapter.log.debug( `Could not parse action string ${String(event.action)}: ${String(e2)}` ); } } } else { event.message = valStr; } } } else { return; } break; case "popup": if (state.val) { try { const popup = JSON.parse(state.val); for (const key of Object.keys(popup)) { event[key] = popup[key]; } } catch (e) { this.adapter.log.error(`Could not parse popup object: ${String(e)}`); return; } } else { return; } break; case "popup_close": if (state.val) { event.command = "close_popup"; } else { return; } break; case "refresh": break; case "set_theme": if (state.val) { const valStr = state.val; try { const theme = JSON.parse(valStr); for (const key of Object.keys(theme)) { event[key] = theme[key]; } } catch { event.theme = valStr; } } else { return; } break; case "console": if (state.val) { event.message = state.val; } else { return; } break; case "change_browser_id": if (state.val) { const valStr = state.val; event.current_browser_id = browserId; try { const data = JSON.parse(valStr); for (const key of Object.keys(data)) { event[key] = data[key]; } } catch { event.new_browser_id = valStr; } } else { return; } break; case "hideHeader": case "hideSidebar": { const key = command; const val = !!state.val; if (allDevices) { this.browserModStorage.settings[key] = val; void this._applyRootSettingToAll(key, val); } else { this.initialiseBrowserSettings(browserId); this.browserModStorage.browsers[browserId].settings[key] = val; } event = { result: this.browserModStorage }; break; } default: return; } if (allDevices) { for (const c of Object.values(this.clients)) { this._sendToClient(c, { type: "event", event, id: c.subscribeId }); } } else { event.browserID = client.instance; this._sendToClient(client, { type: "event", event, id: client.subscribeId }); } } else { this.adapter.log.warn(`Device ${browserId} currently not connected. Can not send command ${command}`); } } } /** * Handle a change of lovelace configuration. Updates ioBroker objects for new views. * * @param lovelaceConfig - current lovelace configuration * @param lovelaceConfig.views - array of view definitions with path property */ handeUpdatedConfig(lovelaceConfig) { let needUpdate = false; for (const view of lovelaceConfig.views) { const viewPath = `/lovelace/${view.path}`; if (!this.knownViews.includes(viewPath)) { needUpdate = true; this.knownViews.push(viewPath); } } if (needUpdate) { this.knownViewsStates = {}; for (let i = 0; i < this.knownViews.length; i += 1) { this.knownViewsStates[this.knownViews[i]] = this.knownViews[i]; } for (const id of Object.keys(this.objects)) { if (id.startsWith(`${this.adapter.namespace}.${instancesPath}`) && id.endsWith(".path")) { this.adapter.extendObject(id, { common: { type: "string", states: this.knownViewsStates } }, () => { this.adapter.log.debug(`Updated ${id}`); }); } } } } /** * Initialize the browser_mod module. * * @param lovelaceConfig - current lovelace configuration * @param lovelaceConfig.views - array of view definitions with path property */ async init(lovelaceConfig) { this.handeUpdatedConfig(lovelaceConfig); await this._checkObjects(instancesPath.substring(0, instancesPath.length - 1)); for (const id of Object.keys(this.objects)) { if (id.startsWith(`${this.adapter.namespace}.${instancesPath}`)) { const browserId = id.split(".")[3]; if (id.endsWith(".online")) { const onlineState = await this.adapter.getStateAsync(id); this.initialiseBrowserSettings(browserId); this.browserModStorage.browsers[browserId].last_seen = (onlineState == null ? void 0 : onlineState.lc) || 0; await this.adapter.setState(id, false, true); } else { for (const key of ["hideHeader", "hideSidebar"]) { if (id.endsWith(key)) { const settingState = await this.adapter.getStateAsync(id); if (settingState) { if (id === `${this.adapter.namespace}.${instancesPath}${key}`) { this.browserModStorage.settings[key] = settingState.val; } else { this.initialiseBrowserSettings(browserId); this.browserModStorage.browsers[browserId].settings[key] = settingState.val; } } } } } } } await this._cleanUpInstances(); this.adapter.log.debug("modules/browser_mod: init done."); } /** * Augment the services object with browser_mod services. * * @param services - services map to augment */ augmentServices(services) { const target = { device: [{ integration: "browser_mod" }], entity: [{ integration: "browser_mod" }] }; services.browser_mod = { sequence: { name: "", description: "Run a sequence of services", fields: { sequence: { name: "Actions", description: "List of services to run", selector: { object: null } } }, target }, delay: { name: "", description: "Wait for a time", fields: { time: { name: "Time", description: "Time to wait (ms)", selector: { number: { mode: "box" } } } }, target }, popup: { name: "", description: "Display a popup", fields: { title: { name: "Title", description: "Popup title", selector: { text: null } }, content: { name: "Content", required: true, description: "Popup content (Test or lovelace card configuration)", selector: { object: null } }, size: { name: "Size", description: "", selector: { select: { mode: "dropdown", options: ["normal", "wide", "fullscreen"] } } }, right_button: { name: "Right button", description: "Text of the right button", selector: { text: null } }, right_button_action: { name: "Right button action", description: "Action to perform when the right button is pressed", selector: { object: null } }, left_button: { name: "Left button", description: "Text of the left button", selector: { text: null } }, left_button_action: { name: "Left button action", description: "Action to perform when left button is pressed", selector: { object: null } }, dismissable: { name: "User dismissable", description: "Whether the popup can be closed by the user without action", default: true, selector: { boolean: null } }, dismiss_action: { name: "Dismiss action", description: "Action to perform when popup is dismissed", selector: { object: null } }, autoclose: { name: "Auto close", description: "Close the popup automatically on mouse, pointer or keyboard activity", default: false, selector: { boolean: null } }, timeout: { name: "Auto close timeout", description: "Time before closing (ms)", selector: { number: { mode: "box" } } }, timeout_action: { name: "Timeout action", description: "Action to perform when popup is closed by timeout", selector: { object: null } }, style: { name: "Styles", description: "CSS code to apply to the popup window", selector: { text: { multiline: true } } } }, target }, more_info: { name: "", description: "Show more-info dialog", fields: { entity: { name: "Entity ID", description: "", required: true, selector: { text: null } }, large: { name: "Large size", description: "", default: false, selector: { boolean: null } }, ignore_popup_card: { name: "Ignore any active popup-card overrides", description: "", default: false, selector: { boolean: null } } }, target }, close_popup: { name: "", description: "Close a popup", fields: {}, target }, notification: { name: "", description: "Display a short notification", fields: { message: { name: "Message", description: "Message to display", required: true, selector: { text: null } }, duration: { name: "Auto close timeout", description: "Time before closing (ms)", selector: { number: { mode: "box" } } }, action_text: { name: "Action button text", description: "Text of optional action button", selector: { text: null } }, action: { name: "Button action", description: "Action to perform when the action button is pressed", selector: { object: null } } }, target }, navigate: { name: "", description: "Navigate browser to a different page", fields: { path: { name: "Path", description: "Target path", selector: { text: null } } }, target }, refresh: { name: "", description: "Refresh page", fields: {}, target }, set_theme: { name: "", description: "Change the current theme", fields: { theme: { name: "Theme", description: "Name of theme or 'auto'", selector: { text: null } }, dark: { name: "Mode", description: "Dark/light mode", selector: { select: { options: ["auto", "light", "dark"] } } }, primaryColor: { name: "Primary Color", description: "Primary theme color", selector: { color_rgb: null } }, accentColor: { name: "Accent Color", description: "Accent theme color", selector: { color_rgb: null } } }, target }, console: { name: "", description: "Print text to browser console", fields: { message: { name: "Message", description: "Text to print", selector: { text: null } } }, target }, javascript: { name: "", description: "Run arbitrary JavaScript code", fields: { code: { name: "Code", description: "JavaScript code to run", selector: { object: null } } }, target } }; } } module.exports = BrowserModModule; //# sourceMappingURL=browser_mod.js.map