UNPKG

iobroker.lovelace

Version:

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

290 lines (273 loc) 11.2 kB
const WebSocket = require('ws').WebSocket; /** * Persistent Notifications Module. Supplies persistent notifications to the frontend, storing in ioBroker state. */ class PersistentNotificationsModule { /** * Constructor * * @param options options object with adapter and server. */ constructor(options) { this.adapter = options.adapter; this.server = options.server; this._notifications = {}; } /** * Send notification update to client * * @param ws - websocket client * @param id - id of subscription * @param notifications - object with notifications and notification_id as key * @param type - optional type of notification update, default is 'current'. Possible values are 'added', 'removed', 'updated' */ sendNotificationUpdate(ws, id, notifications = {}, type = 'current') { const message = { id: Number(id), type: 'event', event: { type: type, notifications, }, }; ws.send(JSON.stringify(message)); } /** * Publish notification update to all clients with subscription * * @param notifications - object with notifications and notification_id as key * @param type - optional type of notification update, default is 'current'. Possible values are 'added', 'removed', 'updated' */ publishNotificationsUpdate(notifications = {}, type) { const clients = this.server.getClientsWithSubscription('notification'); for (const client of clients) { if (client._subscribes && client._subscribes.notification && client.readyState === WebSocket.OPEN) { for (const id of client._subscribes.notification) { this.sendNotificationUpdate(client, id, notifications, type); } } } } /** * Update the persistent notifications list in the ioBroker state * * @returns {Promise<*|{}>} - updated notifications list */ async _saveNotifications() { await this.adapter.setStateAsync('notifications.list', JSON.stringify(this._notifications), true); return this._notifications; } /** * Read the persistent notifications list from the ioBroker state * * @returns {Promise<*|{}>} - notifications list */ async _readNotifications() { try { const state = await this.adapter.getStateAsync('notifications.list'); const val = (state && state.val) || '{}'; try { this._notifications = JSON.parse(val); if (this._notifications.length) { //need to convert from array to object: const oldNotifications = this._notifications; this._notifications = {}; for (const notification of oldNotifications) { this._notifications[notification.notification_id] = notification; } await this._saveNotifications(); } } catch (e) { this.adapter.log.warn(`Cannot parse notifications: ${val}: ${e} - ${e.stack}`); this._notifications = {}; } return this._notifications; } catch (e) { this.adapter.log.error(`Failed to read notifications: ${e}`); } return this._notifications; } /** * Add a notification to the persistent notifications list * * @param info - notification object or string from frontend * @returns {Promise<*|{}>} - updated notifications list */ async _addNotification(info) { if (typeof info !== 'object') { if (typeof info === 'string' && info.trim()[0] === '{') { try { info = JSON.parse(info); } catch (e) { console.warn(`Cannot parse ${info}: ${e} - ${e.stack}`); info = { message: info.toString() }; } } else { info = { message: info.toString() }; } } let type = 'added'; if (info.notification_id === undefined) { info.notification_id = Date.now(); while (this._notifications[info.notification_id]) { info.notification_id += 1; } } else { type = 'updated'; //notify frontend that this notification was updated. } info.created_at = info.created_at || Date.now(); this._notifications[info.notification_id] = info; this.publishNotificationsUpdate({ [info.notification_id]: info }, type); return this._saveNotifications(); } /** * Clear one or all notifications from the persistent notifications list * * @param [id] - optional id of notification to clear. If not provided, all notifications are cleared. * @returns {Promise<*|{}>} - updated notifications list */ async _clearNotification(id) { if (id) { if (this._notifications[id]) { const removedNotifications = {}; removedNotifications[id] = this._notifications[id]; this.publishNotificationsUpdate(removedNotifications, 'removed'); delete this._notifications[id]; } else { this.publishNotificationsUpdate(this._notifications, 'current'); } } else { this.publishNotificationsUpdate(this._notifications, 'removed'); this._notifications = {}; } return this._saveNotifications(); } /** * Handle subscribe here. * Unsubscribe is generic and is handled in server.js. * * @param ws - websocket connection to the client that sent the message * @param message - message object from frontend * @returns {Promise<boolean>} - true if the message was processed */ async processMessage(ws, message) { if (message.type === 'persistent_notification/subscribe') { try { //handle subscribe here: ws.send(JSON.stringify({ id: Number(message.id), type: 'result', success: true, result: null })); //say that subscription was success ws._subscribes.notification = ws._subscribes.notification || []; ws._subscribes.notification.push(message.id); this.adapter.log.debug(`New notification subscription ${message.id}`); //let's read the current state and send: const notifications = await this._readNotifications(); this.sendNotificationUpdate(ws, message.id, notifications); } catch (e) { this.adapter.log.error(`Could not create notification answer: ${e.stack}`); this.sendNotificationUpdate(ws, message.id, {}); } return true; } //not processed return false; } /** * Process service calls for persistent notifications * * @param ws - websocket connection to the client that sent the service call * @param data - service call object * @returns {Promise<boolean>} - true if the service call was processed */ async processServiceCall(ws, data) { if (data.domain === 'persistent_notification') { if (data.service === 'create') { await this._addNotification(data.service_data); } else if (data.service === 'dismiss') { await this._clearNotification(data.service_data.notification_id); } else if (data.service === 'dismiss_all') { await this._clearNotification(); } return true; } } /** * Handle state change from ioBroker, i.e., allow objects to add / modify / clear messages. * * @param {string} id - id of the state that changed * @param {ioBroker.State|null} state - new state or null if deleted * @returns {Promise<false|*|{}|void>} - true if the state change was processed */ async onStateChange(id, state) { if (id === `${this.adapter.namespace}.notifications.list`) { if (!state?.ack) { await this._readNotifications(); } return this.publishNotificationsUpdate(this._notifications, 'current'); } else if (id === `${this.adapter.namespace}.notifications.add`) { return !state?.ack && this._addNotification(state?.val); } else if (id === `${this.adapter.namespace}.notifications.clear`) { return !state?.ack && this._clearNotification(state?.val); } } /** * Add persistent Notification services to the provided services object * * @param services - object with services */ augmentServices(services) { services.persistent_notification = { create: { name: 'Create', description: 'Shows a notification on the **Notifications** panel.', fields: { message: { required: true, example: 'Please check your configuration.yaml.', selector: { text: null, }, name: 'Message', description: 'Message body of the notification.', }, title: { example: 'Test notification', selector: { text: null, }, name: 'Title', description: 'Optional title of the notification.', }, notification_id: { example: 1234, selector: { text: null, }, name: 'Notification ID', description: 'ID of the notification. This new notification will overwrite an existing notification with the same ID.', }, }, }, dismiss: { name: 'Dismiss', description: 'Removes a notification from the **Notifications** panel.', fields: { notification_id: { required: true, example: 1234, selector: { text: null, }, name: 'Notification ID', description: 'ID of the notification to be removed.', }, }, }, dismiss_all: { name: 'Dismiss all', description: 'Removes all notifications from the **Notifications** panel.', fields: {}, }, }; } } module.exports = PersistentNotificationsModule;