UNPKG

iobroker.lovelace

Version:

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

1,237 lines (1,192 loc) 49.2 kB
const instancesPath = 'instances.'; /** * Support for browser_mod integration. * This is now installed with lovelace by default to control the frontends from iobroker states. */ class BrowserModModule { /** * Create a new instance of the browser_mod module. * * @param options {object} options object with adapter and objects */ constructor(options) { this.adapter = options.adapter; this.objects = options.objects; this.clients = {}; this.browserModStorage = { browsers: {}, version: '2.0', settings: { //possible settings. Send them all. hideSidebar: true, //set this to true for now -> might make change in frontend unnecessary. And maybe we can use it later. hideHeader: false, defaultPanel: null, sidebarPanelOrder: null, sidebarHiddenPanels: null, sidebarTitle: null, faviconTemplate: null, titleTemplate: null, hideInteractIcon: true, //is the icon to configure browser_mod -> not working currently, so let's hide it. autoRegister: true, lockRegister: null, }, user_settings: {}, //hm.. what is this? -> you can select settings on a per user basis. Proably stored here. Let's try that sometime. }; this.knownViews = []; this.knownViewsStates = {}; } /** * Check if all objects for the browser_mod integration are created. * * @param ioBrokerDeviceId {string} ioBroker device id * @param browserId {string} browser id * @param battery {boolean} if battery states should be created * @returns {Promise<void>} promise that resolves when all objects are created */ async _checkObjects(ioBrokerDeviceId, browserId, battery = false) { ioBrokerDeviceId = `${this.adapter.namespace}.${ioBrokerDeviceId}`; //create device / folder: 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 }, }); } //remove old states, if they are still there. Version 3.0.0 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}.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 }, }); } if (!this.objects[`${ioBrokerDeviceId}.hideHeader`]) { await this.adapter.setObjectNotExistsAsync(`${ioBrokerDeviceId}.hideHeader`, { type: 'state', common: { name: 'Hide Header', type: 'boolean', read: true, write: true, role: 'switch', default: this.browserModStorage.settings.hideHeader, }, native: { instance: browserId, }, }); } else { //read hideHeader setting from object: const hideHeader = await this.adapter.getStateAsync(`${ioBrokerDeviceId}.hideHeader`); if (hideHeader) { if (browserId) { this.initialiseBrowserSettings(browserId, true); this.browserModStorage.browsers[browserId].settings.hideHeader = hideHeader.val; } else { this.browserModStorage.settings.hideHeader = hideHeader.val; } } } if (!this.objects[`${ioBrokerDeviceId}.hideSidebar`]) { await this.adapter.setObjectNotExistsAsync(`${ioBrokerDeviceId}.hideSidebar`, { type: 'state', common: { name: 'Hide Sidebar', type: 'boolean', read: true, write: true, role: 'switch', default: this.browserModStorage.settings.hideSidebar, }, native: { instance: browserId, }, }); } else { //read hideSidebar setting from object: const hideSidebar = await this.adapter.getStateAsync(`${ioBrokerDeviceId}.hideSidebar`); if (hideSidebar) { if (browserId) { this.initialiseBrowserSettings(browserId, true); this.browserModStorage.browsers[browserId].settings.hideSidebar = hideSidebar.val; } else { this.browserModStorage.settings.hideSidebar = hideSidebar.val; } } } } /** * Clean up old browser_mod instances, i.e., delete from the object database if too many. * * @returns {Promise<void>} */ 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)}`, ); await this.adapter.delObjectAsync(`${instancesPath}${browserId}`, { recursive: true }); delete this.browserModStorage.browsers[browserId]; } } } /** * Initialise the browser settings for a browser_mod instance. * * @param browserId {string} browser id * @param [now] {boolean} if the browser was seen now */ 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, settings: this.browserModStorage.settings, //send default settings...? meta: 'default', }; } } /** * Handle an update message from a browser_mod instance. Will store infos about the browser instance in ioBroker objects. * * @param ioBrokerDeviceId {string} ioBroker device id * @param message {object} message from browser_mod * @returns {Promise<void>} promise that resolves when the message is handled */ async _handleUpdate(ioBrokerDeviceId, message) { if (message.browserID && this.browserModStorage.browsers[message.browserID]) { this.browserModStorage.browsers[message.browserID].last_seen = Date.now() / 1000; } if (message.data) { if (message.data.browser) { //check if all objects in ioBroker are created. if (message.data.browser.battery_level) { await this._checkObjects(ioBrokerDeviceId, message.browserID, true); await this.adapter.setStateAsync( `${ioBrokerDeviceId}.battery.level`, message.data.browser.battery_level, true, ); await this.adapter.setStateAsync( `${ioBrokerDeviceId}.battery.charging`, message.data.browser.charging || false, true, ); } else { await this._checkObjects(ioBrokerDeviceId, message.browserID); await this._cleanUpInstances(); //maybe delete old instance, if new was created. } if (message.data.browser.path) { await this.adapter.setStateAsync(`${ioBrokerDeviceId}.path`, message.data.browser.path, true); } if (message.data.browser.visibility) { await this.adapter.setStateAsync( `${ioBrokerDeviceId}.visible`, message.data.browser.visibility === 'visible', true, ); } } if (typeof message.data.activity === 'boolean') { await this.adapter.setStateAsync(`${ioBrokerDeviceId}.activity`, message.data.activity, true); } if (typeof message.data.screen_on === 'boolean') { await this.adapter.setStateAsync(`${ioBrokerDeviceId}.blackout`, !message.data.screen_on, true); } if (message.data.player) { // volume: 1 (?), muted: boolean, src: '', state === 'stopped' } } } /** * Send a message to a browser_mod instance. * * @param client {object} websocket client connection * @param message {object} message to send */ _sendToClient(client, message) { client.ws.send( JSON.stringify({ id: client.subscribeId, ...message, }), ); } /** * Process a message from a browser_mod instance. * handles everything in browser_mod/ namespace. * * @param ws {object} websocket connection to the client that sent the message * @param message {object} message from the client * @returns {Promise<boolean>} promise that resolves with true if the message was processed */ async processMessage(ws, message) { if (message.type && message.type.startsWith('browser_mod/')) { const method = message.type.split('/')[1]; // console.log('Incoming browser_mod message:'); // console.dir(message, {depth: null}); 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') { // similar to 'subscribe'. ws.on('close', async () => { this.adapter.log.debug(`Instance ${message.browserID} disconnected.`); delete this.clients[message.browserID]; await this.adapter.setStateAsync(`${ioBrokerDeviceId}.online`, false, true); }); this.adapter.log.debug(`Instance ${message.browserID} connected.`); this.clients[message.browserID] = { subscribeId: message.id, instance: message.browserID, ws, }; ws.browserID = message.browserID; //store browserID in ws object to handle recall service call later. 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: new Date().toISOString(), }, }, ]), ); if (this.objects[`${this.adapter.namespace}.${ioBrokerDeviceId}.online`]) { await this.adapter.setStateAsync(`${ioBrokerDeviceId}.online`, true, true); } else { console.log( 'No objects for instance, yet..', `${ioBrokerDeviceId}.online`, this.objects[ioBrokerDeviceId], //this.objects, ); } } else if (method === 'register') { this.initialiseBrowserSettings(message.browserID, true); // if data.browserID exists, browserID is changed -> copy stuff and delete old id. if (message.data && message.data.browserID) { const newIoBrokerDeviceId = instancesPath + message.data.browserID; for (const id of Object.keys(this.objects)) { if (id.startsWith(ioBrokerDeviceId)) { delete this.objects[id]; } } try { await this.adapter.delObjectAsync(ioBrokerDeviceId, { recursive: true }); await this._checkObjects(newIoBrokerDeviceId, message.data.browserID); } catch (e) { this.adapter.log.warn( `Could not delete old instance objects in ${ ioBrokerDeviceId }, please do so yourself. Error was: ${e}`, ); } delete this.browserModStorage.browsers[message.browserID]; delete message.data.browserID; this.browserModStorage.browsers[message.data.browserID] = message.data; } else { try { await this._checkObjects(ioBrokerDeviceId, message.browserID); await this._cleanUpInstances(); //maybe delete old instance, if new was created. } catch (e) { this.adapter.log.warn( `Could not create objects for instance ${ioBrokerDeviceId}. Error was: ${e}`, ); } if (this.objects[`${this.adapter.namespace}.${ioBrokerDeviceId}.online`]) { await this.adapter.setStateAsync(`${ioBrokerDeviceId}.online`, true, true); } else { console.log( 'No objects for instance, yet..', `${ioBrokerDeviceId}.online`, this.objects[ioBrokerDeviceId], //this.objects, ); } } } else if (method === 'log') { this.adapter.log.debug(`Message from browser_mod: ${message.message}`); ws.send(JSON.stringify({ id: message.id, type: 'result', success: true })); } else if (method === 'settings') { //{"type":"browser_mod/settings","key":"autoRegister","value":true,"id":200} <- something like this... if (message.key && this.browserModStorage.settings[message.key] !== undefined) { this.browserModStorage.settings[message.key] = message.value; this.adapter.log.debug(`Updated browser_mod settings: ${message.key} to ${message.value}`); } //think about making this permanent somehow? -> but only if we find a way to allow browser_mod_settings panel. ws.send(JSON.stringify({ id: message.id, type: 'result', success: true })); } else if (method === 'recall_id') { ws.send(JSON.stringify({ id: message.id, type: 'result', success: true, result: ws.browserID })); } 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: `, 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 {string} id of the state that changed * @param state {ioBroker.State|null} new state or null if deleted * @returns {Promise<void>} promise that resolves when the state change is handled */ async onStateChange(id, state) { //console.log(id); if (state && !state.ack && id.startsWith(`${this.adapter.namespace}.${instancesPath}`)) { //ok, is relevant for us. 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: 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: ${e}`); if (state.val.includes(';')) { [event.duration, event.message, event.action_text, event.action] = state.val.split(';'); if (event.action) { try { event.action = JSON.parse(event.action); } catch (e) { this.adapter.debug(`Could not parse action string ${event.action}: ${e}`); } } } else { event.message = state.val; } } } 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: ', e); return; } } else { return; } break; case 'popup_close': if (state.val) { event.command = 'close_popup'; } else { return; } break; case 'refresh': break; case 'hideHeader': if (allDevices) { this.browserModStorage.settings.hideHeader = state.val; } else { this.browserModStorage.browsers[browserId].hideHeader = state.val; } event = { result: this.browserModStorage, }; break; default: return; } //console.log('sending: ', event); if (allDevices) { for (const client of Object.values(this.clients)) { this._sendToClient(client, { type: 'event', event, id: client.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. * Will update ioBroker objects for new views. * * @param lovelaceConfig {object} new lovelace configuration * @returns {Promise<void>} promise that resolves when the configuration change is handled */ async 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. * Will update ioBroker objects for new views. * * @param lovelaceConfig {object} lovelace configuration * @returns {Promise<void>} promise that resolves when the module is initialized */ async init(lovelaceConfig) { await this.handeUpdatedConfig(lovelaceConfig); await this._checkObjects(instancesPath.substring(0, instancesPath.length - 1)); //iterate all instances in object database and initialize browser settings. Also reads hideHeader states. //initiates browser online state to false. 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?.lc || 0; await this.adapter.setState(id, false, true); } else if (id.endsWith('hideHeader')) { const hideHeader = await this.adapter.getStateAsync(id); if (hideHeader) { if (id === `${this.adapter.namespace}.${instancesPath}hideHeader`) { this.browserModStorage.settings.hideHeader = hideHeader.val; } else { //read hideHeader states from ioBroker objects. this.initialiseBrowserSettings(browserId); this.browserModStorage.browsers[id.split('.')[3]].settings.hideHeader = hideHeader.val; } } } } } //maybe clean up instances here await this._cleanUpInstances(); } /** * Augment the services object with browser_mod services. * * @param services {object} services object to augment */ augmentServices(services) { 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: { device: [ { integration: 'browser_mod', }, ], entity: [ { integration: 'browser_mod', }, ], }, }, delay: { name: '', description: 'Wait for a time', fields: { time: { name: 'Time', description: 'Time to wait (ms)', selector: { number: { mode: 'box', }, }, }, }, target: { device: [ { integration: 'browser_mod', }, ], entity: [ { integration: 'browser_mod', }, ], }, }, 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: { device: [ { integration: 'browser_mod', }, ], entity: [ { integration: 'browser_mod', }, ], }, }, 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: { device: [ { integration: 'browser_mod', }, ], entity: [ { integration: 'browser_mod', }, ], }, }, close_popup: { name: '', description: 'Close a popup', fields: {}, target: { device: [ { integration: 'browser_mod', }, ], entity: [ { integration: 'browser_mod', }, ], }, }, 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: { device: [ { integration: 'browser_mod', }, ], entity: [ { integration: 'browser_mod', }, ], }, }, navigate: { name: '', description: 'Navigate browser to a different page', fields: { path: { name: 'Path', description: 'Target path', selector: { text: null, }, }, }, target: { device: [ { integration: 'browser_mod', }, ], entity: [ { integration: 'browser_mod', }, ], }, }, refresh: { name: '', description: 'Refresh page', fields: {}, target: { device: [ { integration: 'browser_mod', }, ], entity: [ { integration: 'browser_mod', }, ], }, }, 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: { device: [ { integration: 'browser_mod', }, ], entity: [ { integration: 'browser_mod', }, ], }, }, console: { name: '', description: 'Print text to browser console', fields: { message: { name: 'Message', description: 'Text to print', selector: { text: null, }, }, }, target: { device: [ { integration: 'browser_mod', }, ], entity: [ { integration: 'browser_mod', }, ], }, }, javascript: { name: '', description: 'Run arbitrary JavaScript code', fields: { code: { name: 'Code', description: 'JavaScript code to run', selector: { object: null, }, }, }, target: { device: [ { integration: 'browser_mod', }, ], entity: [ { integration: 'browser_mod', }, ], }, }, }; } } module.exports = BrowserModModule;