UNPKG

@iobroker/adapter-react-v5

Version:

React components to develop ioBroker interfaces with react.

1,390 lines 106 kB
/** Possible progress states. */ export const PROGRESS = { /** The socket is connecting. */ CONNECTING: 0, /** The socket is successfully connected. */ CONNECTED: 1, /** All objects are loaded. */ OBJECTS_LOADED: 2, /** All states are loaded. */ STATES_LOADED: 3, /** The socket is ready for use. */ READY: 4, }; const PERMISSION_ERROR = 'permissionError'; const NOT_CONNECTED = 'notConnectedError'; export const ERRORS = { PERMISSION_ERROR, NOT_CONNECTED, }; /** Converts ioB pattern into regex */ export function pattern2RegEx(pattern) { pattern = (pattern || '').toString(); const startsWithWildcard = pattern[0] === '*'; const endsWithWildcard = pattern[pattern.length - 1] === '*'; pattern = pattern.replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&').replace(/\*/g, '.*'); return (startsWithWildcard ? '' : '^') + pattern + (endsWithWildcard ? '' : '$'); } export class LegacyConnection { // Do not define it as null, else we must check for null everywhere _socket; _authTimer; systemLang = 'en'; _waitForFirstConnection; _waitForFirstConnectionResolve = null; _promises = {}; _instanceSubscriptions; props; doNotLoadAllObjects; doNotLoadACL; states = {}; objects = null; scriptLoadCounter; acl = null; firstConnect = true; waitForRestart = false; connected = false; statesSubscribes = {}; objectsSubscribes = {}; filesSubscribes = {}; onConnectionHandlers = []; onLogHandlers = []; onProgress; onError; loaded = false; loadTimer = null; loadCounter = 0; ignoreState = ''; simStates = {}; autoSubscribes; autoSubscribeLog; subscribed; isSecure; onCmdStdoutHandler; onCmdStderrHandler; onCmdExitHandler; systemConfig = null; objectViewCached; constructor(props) { props ||= { protocol: window.location.protocol, host: window.location.hostname }; this.props = props; this.autoSubscribes = this.props.autoSubscribes || []; this.autoSubscribeLog = this.props.autoSubscribeLog || false; this.props.protocol ||= window.location.protocol; this.props.host ||= window.location.hostname; this.props.port ||= window.location.port === '3000' ? (LegacyConnection.isWeb() ? 8082 : 8081) : window.location.port; this.props.ioTimeout = Math.max(this.props.ioTimeout || 20000, 20000); this.props.cmdTimeout = Math.max(this.props.cmdTimeout || 5000, 5000); this._instanceSubscriptions = {}; // breaking change. Do not load all objects by default is true this.doNotLoadAllObjects = this.props.doNotLoadAllObjects === undefined ? true : this.props.doNotLoadAllObjects; this.doNotLoadACL = this.props.doNotLoadACL === undefined ? true : this.props.doNotLoadACL; this.states = {}; this._waitForFirstConnection = new Promise(resolve => { this._waitForFirstConnectionResolve = resolve; }); this.onProgress = this.props.onProgress || (() => { }); this.onError = this.props.onError || ((err) => console.error(err)); this.startSocket(); } /** * Checks if this connection is running in a web adapter and not in an admin. * * @returns True if running in a web adapter or in a socketio adapter. */ static isWeb() { const adapterName = window.adapterName; return (adapterName === 'material' || adapterName === 'vis' || adapterName?.startsWith('vis-') || adapterName === 'echarts-show' || window.socketUrl !== undefined); } /** * Starts the socket.io connection. */ startSocket() { // if socket io is not yet loaded if (typeof window.io === 'undefined' && typeof window.iob === 'undefined') { // if in index.html the onLoad function not defined if (typeof window.registerSocketOnLoad !== 'function') { // poll if loaded this.scriptLoadCounter ||= 0; this.scriptLoadCounter++; if (this.scriptLoadCounter < 30) { // wait till the script loaded setTimeout(() => this.startSocket(), 100); return; } window.alert('Cannot load socket.io.js!'); } else { // register on load window.registerSocketOnLoad(() => this.startSocket()); } return; } if (this._socket) { // socket was initialized, do not repeat return; } let host = this.props.host; let port = this.props.port; let protocol = this.props.protocol.replace(':', ''); let path = window.location.pathname; if (window.location.hostname === 'iobroker.net' || window.location.hostname === 'iobroker.pro') { path = ''; } else { // if web adapter, socket io could be on another port or even host if (window.socketUrl) { const parsed = new URL(window.socketUrl); host = parsed.hostname; port = parsed.port; protocol = parsed.protocol.replace(':', ''); } // get a current path const pos = path.lastIndexOf('/'); if (pos !== -1) { path = path.substring(0, pos + 1); } if (LegacyConnection.isWeb()) { // remove one level, like echarts, vis, .... We have here: '/echarts/' const parts = path.split('/'); if (parts.length > 2) { parts.pop(); // if it is a version, like in material, so remove it too if (parts[parts.length - 1].match(/\d+\.\d+\.\d+/)) { parts.pop(); } parts.pop(); path = parts.join('/'); if (!path.endsWith('/')) { path += '/'; } } } } const url = port ? `${protocol}://${host}:${port}${path}` : `${protocol}://${host}${path}`; this._socket = (window.io || window.iob).connect(url, { path: path.endsWith('/') ? `${path}socket.io` : `${path}/socket.io`, query: 'ws=true', name: this.props.name, timeout: this.props.ioTimeout, uuid: this.props.uuid, }); this._socket.on('connect', (noTimeout) => { // If the user is not admin, it takes some time to install the handlers, because all rights must be checked if (noTimeout !== true) { setTimeout(() => this.getVersion().then(info => { const [major, minor, patch] = info.version.split('.'); const v = parseInt(major, 10) * 10000 + parseInt(minor, 10) * 100 + parseInt(patch, 10); if (v < 40102) { this._authTimer = null; // possible this is an old version of admin this.onPreConnect(false, false); } else { this._socket.emit('authenticate', (isOk, isSecure) => this.onPreConnect(isOk, isSecure)); } }), 500); } else { // iobroker websocket waits, till all handlers are installed this._socket.emit('authenticate', (isOk, isSecure) => this.onPreConnect(isOk, isSecure)); } }); this._socket.on('reconnect', () => { this.onProgress(PROGRESS.READY); this.connected = true; if (this.waitForRestart) { window.location.reload(); } else { this._subscribe(true); this.onConnectionHandlers.forEach(cb => cb(true)); } }); this._socket.on('disconnect', () => { this.connected = false; this.subscribed = false; this.onProgress(PROGRESS.CONNECTING); this.onConnectionHandlers.forEach(cb => cb(false)); }); this._socket.on('reauthenticate', () => LegacyConnection.authenticate()); this._socket.on('log', message => { this.props.onLog?.(message); this.onLogHandlers.forEach(cb => cb(message)); }); this._socket.on('error', (err) => { let _err = err || ''; if (typeof _err.toString !== 'function') { _err = JSON.stringify(_err); console.error(`Received strange error: ${_err}`); } _err = _err.toString(); if (_err.includes('User not authorized')) { LegacyConnection.authenticate(); } else { window.alert(`Socket Error: ${err}`); } }); this._socket.on('connect_error', (err) => console.error(`Connect error: ${err}`)); this._socket.on('permissionError', (err) => this.onError({ message: 'no permission', operation: err.operation, type: err.type, id: err.id || '', })); this._socket.on('objectChange', (id, obj) => setTimeout(() => this.objectChange(id, obj), 0)); this._socket.on('stateChange', (id, state) => setTimeout(() => this.stateChange(id, state), 0)); this._socket.on('im', (messageType, from, data) => setTimeout(() => this.instanceMessage(messageType, from, data), 0)); this._socket.on('fileChange', (id, fileName, size) => setTimeout(() => this.fileChange(id, fileName, size), 0)); this._socket.on('cmdStdout', (id, text) => this.onCmdStdoutHandler?.(id, text)); this._socket.on('cmdStderr', (id, text) => this.onCmdStderrHandler?.(id, text)); this._socket.on('cmdExit', (id, exitCode) => this.onCmdExitHandler?.(id, exitCode)); } /** * Called internally. */ onPreConnect(_isOk, isSecure) { if (this._authTimer) { clearTimeout(this._authTimer); this._authTimer = null; } this.connected = true; this.isSecure = isSecure; if (this.waitForRestart) { window.location.reload(); } else { if (this.firstConnect) { // retry strategy this.loadTimer = setTimeout(() => { this.loadTimer = null; this.loadCounter++; if (this.loadCounter < 10) { void this.onConnect().catch(e => this.onError(e)); } }, 1000); if (!this.loaded) { void this.onConnect().catch(e => this.onError(e)); } } else { this.onProgress(PROGRESS.READY); } this._subscribe(true); this.onConnectionHandlers.forEach(cb => cb(true)); } if (this._waitForFirstConnectionResolve) { this._waitForFirstConnectionResolve(); this._waitForFirstConnectionResolve = null; } } /** * Checks if running in ioBroker cloud */ static isCloud() { if (window.location.hostname.includes('amazonaws.com') || window.location.hostname.includes('iobroker.in')) { return true; } if (typeof window.socketUrl === 'undefined') { return false; } return window.socketUrl.includes('iobroker.in') || window.socketUrl.includes('amazonaws'); } /** * Checks if the socket is connected. */ isConnected() { return this.connected; } /** * Checks if the socket is connected. * Promise resolves if once connected. */ waitForFirstConnection() { return this._waitForFirstConnection; } /** * Called internally. */ async _getUserPermissions() { if (this.doNotLoadACL) { return null; } return new Promise((resolve, reject) => { this._socket.emit('getUserPermissions', (err, acl) => err ? reject(new Error(err)) : resolve(acl)); }); } /** * Called internally. */ async onConnect() { let acl; try { acl = await this._getUserPermissions(); } catch (e) { const knownError = e; this.onError(`Cannot read user permissions: ${knownError.message}`); return; } if (!this.doNotLoadACL) { if (this.loaded) { return; } this.loaded = true; if (this.loadTimer) { clearTimeout(this.loadTimer); this.loadTimer = null; } this.onProgress(PROGRESS.CONNECTED); this.firstConnect = false; this.acl = acl; } // Read system configuration let systemConfig; try { systemConfig = await this.getSystemConfig(); if (this.doNotLoadACL) { if (this.loaded) { return; } this.loaded = true; if (this.loadTimer) { clearTimeout(this.loadTimer); this.loadTimer = null; } this.onProgress(PROGRESS.CONNECTED); this.firstConnect = false; } this.systemConfig = systemConfig; if (this.systemConfig?.common) { this.systemLang = this.systemConfig.common.language; } else { // @ts-expect-error userLanguage is not standard this.systemLang = window.navigator.userLanguage || window.navigator.language; if (/^(en|de|ru|pt|nl|fr|it|es|pl|uk)-?/.test(this.systemLang)) { this.systemLang = this.systemLang.substr(0, 2); } else if (!/^(en|de|ru|pt|nl|fr|it|es|pl|uk|zh-cn)$/.test(this.systemLang)) { this.systemLang = 'en'; } } this.props.onLanguage?.(this.systemLang); if (!this.doNotLoadAllObjects) { await this.getObjects(); this.onProgress(PROGRESS.READY); if (this.props.onReady && this.objects) { this.props.onReady(this.objects); } } else { this.objects = { 'system.config': systemConfig }; this.onProgress(PROGRESS.READY); this.props.onReady?.(this.objects); } } catch (e) { this.onError(`Cannot read system config: ${e}`); } } /** * Called internally. */ static authenticate() { if (window.location.search.includes('&href=')) { window.location.href = `${window.location.protocol}//${window.location.host}${window.location.pathname}${window.location.search}${window.location.hash}`; } else { window.location.href = `${window.location.protocol}//${window.location.host}${window.location.pathname}?login&href=${window.location.search}${window.location.hash}`; } } /** * Subscribe to changes of the given state. * * @param id The ioBroker state ID or array of states * @param binary Set to true if the given state is binary and requires Base64 decoding * @param cb The callback */ async subscribeState(id, binary, cb) { if (typeof binary === 'function') { cb = binary; binary = false; } let ids; if (!Array.isArray(id)) { ids = [id]; } else { ids = id; } if (!cb) { console.error('No callback found for subscribeState'); return Promise.reject(new Error('No callback found for subscribeState')); } const toSubscribe = []; for (let i = 0; i < ids.length; i++) { const _id = ids[i]; if (!this.statesSubscribes[_id]) { let reg = _id .replace(/\./g, '\\.') .replace(/\*/g, '.*') .replace(/\(/g, '\\(') .replace(/\)/g, '\\)') .replace(/\+/g, '\\+') .replace(/\[/g, '\\['); if (!reg.includes('*')) { reg += '$'; } this.statesSubscribes[_id] = { reg: new RegExp(reg), cbs: [cb] }; if (_id !== this.ignoreState) { toSubscribe.push(_id); } } else { !this.statesSubscribes[_id].cbs.includes(cb) && this.statesSubscribes[_id].cbs.push(cb); } } if (!this.connected) { return; } if (toSubscribe.length) { // no answer from server required this._socket.emit('subscribe', toSubscribe); } if (binary) { let base64; for (let i = 0; i < ids.length; i++) { try { // deprecated, but we still support it base64 = await this.getBinaryState(ids[i]); } catch (e) { console.error(`Cannot getBinaryState "${ids[i]}": ${JSON.stringify(e)}`); base64 = undefined; } if (base64 !== undefined && cb) { cb(ids[i], base64); } } } else { return new Promise((resolve, reject) => { this._socket.emit(LegacyConnection.isWeb() ? 'getStates' : 'getForeignStates', ids, (err, states) => { if (err) { console.error(`Cannot getForeignStates "${id}": ${JSON.stringify(err)}`); reject(new Error(err)); } else { if (states) { Object.keys(states).forEach(_id => cb(_id, states[_id])); } resolve(); } }); }); } } /** * Subscribe to changes of the given state. */ subscribeStateAsync( /** The ioBroker state ID or array of states */ id, /** The callback. */ cb) { let ids; if (!Array.isArray(id)) { ids = [id]; } else { ids = id; } const toSubscribe = []; for (let i = 0; i < ids.length; i++) { const _id = ids[i]; if (!this.statesSubscribes[_id]) { let reg = _id .replace(/\./g, '\\.') .replace(/\*/g, '.*') .replace(/\(/g, '\\(') .replace(/\)/g, '\\)') .replace(/\+/g, '\\+') .replace(/\[/g, '\\['); if (!reg.includes('*')) { reg += '$'; } this.statesSubscribes[_id] = { reg: new RegExp(reg), cbs: [] }; this.statesSubscribes[_id].cbs.push(cb); if (_id !== this.ignoreState) { // no answer from server required toSubscribe.push(_id); } } else { !this.statesSubscribes[_id].cbs.includes(cb) && this.statesSubscribes[_id].cbs.push(cb); } } if (toSubscribe.length && this.connected) { // no answer from server required this._socket.emit('subscribe', toSubscribe); } return new Promise((resolve, reject) => { if (typeof cb === 'function' && this.connected) { this._socket.emit(LegacyConnection.isWeb() ? 'getStates' : 'getForeignStates', id, (err, states) => { err && console.error(`Cannot getForeignStates "${id}": ${JSON.stringify(err)}`); states && Object.keys(states).forEach(_id => cb(_id, states[_id])); states ? resolve() : reject(new Error(`Cannot getForeignStates "${id}": ${JSON.stringify(err)}`)); }); } else { this.connected ? reject(new Error('callback is not a function')) : reject(new Error('not connected')); } }); } /** * Unsubscribes all or the given callback from changes of the given state. */ unsubscribeState( /** The ioBroker state ID or array of states */ id, /** The callback. */ cb) { let ids; if (!Array.isArray(id)) { ids = [id]; } else { ids = id; } const toUnsubscribe = []; for (let i = 0; i < ids.length; i++) { const _id = ids[i]; if (this.statesSubscribes[_id]) { if (cb) { const pos = this.statesSubscribes[_id].cbs.indexOf(cb); if (pos !== -1) { this.statesSubscribes[_id].cbs.splice(pos, 1); } } else { this.statesSubscribes[_id].cbs = []; } if (!this.statesSubscribes[_id].cbs || !this.statesSubscribes[_id].cbs.length) { delete this.statesSubscribes[_id]; if (_id !== this.ignoreState) { toUnsubscribe.push(_id); } } } } if (toUnsubscribe.length && this.connected) { // no answer from server required this._socket.emit('unsubscribe', toUnsubscribe); } } /** * Subscribe to changes of the given object. * * @param id The ioBroker object ID or array of objects * @param cb The callback */ subscribeObject(id, cb) { let ids; if (!Array.isArray(id)) { ids = [id]; } else { ids = id; } const toSubscribe = []; for (let i = 0; i < ids.length; i++) { const _id = ids[i]; if (!this.objectsSubscribes[_id]) { let reg = _id.replace(/\./g, '\\.').replace(/\*/g, '.*'); if (!reg.includes('*')) { reg += '$'; } this.objectsSubscribes[_id] = { reg: new RegExp(reg), cbs: [cb] }; toSubscribe.push(_id); } else { !this.objectsSubscribes[_id].cbs.includes(cb) && this.objectsSubscribes[_id].cbs.push(cb); } } if (this.connected && toSubscribe.length) { this._socket.emit('subscribeObjects', toSubscribe); } return Promise.resolve(); } /** * Unsubscribes all or the given callback from changes of the given object. * * @param id The ioBroker object ID or array of objects * @param cb The callback */ unsubscribeObject(id, cb) { let ids; if (!Array.isArray(id)) { ids = [id]; } else { ids = id; } const toUnsubscribe = []; for (let i = 0; i < ids.length; i++) { const _id = ids[i]; if (this.objectsSubscribes[_id]) { if (cb) { const pos = this.objectsSubscribes[_id].cbs.indexOf(cb); pos !== -1 && this.objectsSubscribes[_id].cbs.splice(pos, 1); } else { this.objectsSubscribes[_id].cbs = []; } if (this.connected && (!this.objectsSubscribes[_id].cbs || !this.objectsSubscribes[_id].cbs.length)) { delete this.objectsSubscribes[_id]; toUnsubscribe.push(_id); } } } if (this.connected && toUnsubscribe.length) { this._socket.emit('unsubscribeObjects', toUnsubscribe); } return Promise.resolve(); } /** * Called internally. */ fileChange(id, fileName, size) { for (const sub of Object.values(this.filesSubscribes)) { if (sub.regId.test(id) && sub.regFilePattern.test(fileName)) { for (const cb of sub.cbs) { try { cb(id, fileName, size); } catch (e) { console.error(`Error by callback of fileChange: ${e}`); } } } } } /** * Subscribe to changes of the files. * * @param id The ioBroker state ID for meta-object. Could be a pattern * @param filePattern Pattern or file name, like 'main/*' or 'main/visViews.json` * @param cb The callback. */ async subscribeFiles( /** The ioBroker state ID for meta-object. Could be a pattern */ id, /** Pattern or file name, like 'main/*' or 'main/visViews.json` */ filePattern, /** The callback. */ cb) { if (typeof cb !== 'function') { throw new Error('The state change handler must be a function!'); } let filePatterns; if (Array.isArray(filePattern)) { filePatterns = filePattern; } else { filePatterns = [filePattern]; } const toSubscribe = []; for (let f = 0; f < filePatterns.length; f++) { const pattern = filePatterns[f]; const key = `${id}$%$${pattern}`; if (!this.filesSubscribes[key]) { this.filesSubscribes[key] = { regId: new RegExp(pattern2RegEx(id)), regFilePattern: new RegExp(pattern2RegEx(pattern)), cbs: [cb], }; toSubscribe.push(pattern); } else { !this.filesSubscribes[key].cbs.includes(cb) && this.filesSubscribes[key].cbs.push(cb); } } if (this.connected && toSubscribe.length) { this._socket.emit('subscribeFiles', id, toSubscribe); } return Promise.resolve(); } /** * Unsubscribes the given callback from changes of files. * * @param id The ioBroker state ID. * @param filePattern Pattern or file name, like 'main/*' or 'main/visViews.json` * @param cb The callback. */ unsubscribeFiles(id, filePattern, cb) { let filePatterns; if (Array.isArray(filePattern)) { filePatterns = filePattern; } else { filePatterns = [filePattern]; } const toUnsubscribe = []; for (let f = 0; f < filePatterns.length; f++) { const pattern = filePatterns[f]; const key = `${id}$%$${pattern}`; if (this.filesSubscribes[key]) { const sub = this.filesSubscribes[key]; if (cb) { const pos = sub.cbs.indexOf(cb); pos !== -1 && sub.cbs.splice(pos, 1); } else { sub.cbs = []; } if (!sub.cbs?.length) { delete this.filesSubscribes[key]; if (this.connected) { toUnsubscribe.push(pattern); } } } } if (this.connected && toUnsubscribe.length) { this._socket.emit('unsubscribeFiles', id, toUnsubscribe); } } /** * Called internally. */ objectChange(id, obj) { // update main.objects cache if (!this.objects) { return; } let oldObj; let changed = false; if (obj) { if (this.objects[id]) { // @ts-expect-error fix later oldObj = { _id: id, type: this.objects[id].type }; } if (!this.objects[id] || JSON.stringify(this.objects[id]) !== JSON.stringify(obj)) { this.objects[id] = obj; changed = true; } } else if (this.objects[id]) { // @ts-expect-error fix later oldObj = { _id: id, type: this.objects[id].type }; delete this.objects[id]; changed = true; } Object.keys(this.objectsSubscribes).forEach(_id => { if (_id === id || this.objectsSubscribes[_id].reg.test(id)) { this.objectsSubscribes[_id].cbs.forEach(cb => { try { cb(id, obj, oldObj); } catch (e) { console.error(`Error by callback of objectChange: ${e}`); } }); } }); if (changed && this.props.onObjectChange) { void this.props.onObjectChange(id, obj); } } /** * Called internally. */ stateChange(id, state) { for (const task in this.statesSubscribes) { if (Object.prototype.hasOwnProperty.call(this.statesSubscribes, task) && this.statesSubscribes[task].reg.test(id)) { this.statesSubscribes[task].cbs.forEach(cb => { try { void cb(id, state); } catch (e) { const knownError = e; console.error(`Error by callback of stateChange: ${knownError?.message}`); } }); } } } /** * Called internally. * * @param messageType The message type * @param sourceInstance The source instance * @param data Payload */ instanceMessage(messageType, sourceInstance, data) { if (this._instanceSubscriptions[sourceInstance]) { this._instanceSubscriptions[sourceInstance].forEach(sub => { if (sub.messageType === messageType) { sub.callback(data, sourceInstance, messageType); } }); } } /** * Gets all states. * * @param pattern The pattern to filter states * @param disableProgressUpdate Don't call onProgress() when done */ getStates(pattern, disableProgressUpdate) { if (!this.connected) { return Promise.reject(new Error(NOT_CONNECTED)); } if (typeof pattern === 'boolean') { disableProgressUpdate = pattern; pattern = undefined; } return new Promise((resolve, reject) => { this._socket.emit('getStates', pattern, (err, res) => { this.states = res; !disableProgressUpdate && this.onProgress(PROGRESS.STATES_LOADED); err ? reject(new Error(err)) : resolve(this.states); }); }); } /** * Gets the given state. * * @param id The state ID */ getState(id) { if (!this.connected) { return Promise.reject(new Error(NOT_CONNECTED)); } if (id && id === this.ignoreState) { return Promise.resolve(this.simStates[id] || { val: null, ack: true }); } return new Promise((resolve, reject) => { this._socket.emit('getState', id, (err, state) => err ? reject(new Error(err)) : resolve(state)); }); } /** * Get the given binary state. * * @deprecated since js-controller 5.0. Use files instead. * @param id The state ID. */ getBinaryState(id) { if (!this.connected) { return Promise.reject(new Error(NOT_CONNECTED)); } // the data will come in base64 return new Promise((resolve, reject) => { this._socket.emit('getBinaryState', id, (err, base64) => err ? reject(new Error(err)) : resolve(base64)); }); } /** * Set the given binary state. * * @deprecated since js-controller 5.0. Use files instead. * @param id The state ID. * @param base64 The Base64 encoded binary data. */ setBinaryState(id, base64) { if (!this.connected) { return Promise.reject(new Error(NOT_CONNECTED)); } // the data will come in base64 return new Promise((resolve, reject) => { this._socket.emit('setBinaryState', id, base64, (err) => err ? reject(new Error(err)) : resolve()); }); } /** * Sets the given state value. * * @param id The state ID * @param val The state value * @param ack The acknowledgment flag */ setState(id, val, ack) { if (!this.connected) { return Promise.reject(new Error(NOT_CONNECTED)); } // extra handling for "nothing_selected" state for vis if (id && id === this.ignoreState) { let state; if (typeof ack === 'boolean') { state = val; } else if (typeof val === 'object' && val.val !== undefined) { state = val; } else { state = { val: val, ack: false, ts: Date.now(), lc: Date.now(), from: 'system.adapter.vis.0', }; } this.simStates[id] = state; // inform subscribers about changes if (this.statesSubscribes[id]) { for (const cb of this.statesSubscribes[id].cbs) { try { void cb(id, state); } catch (e) { console.error(`Error by callback of stateChanged: ${e}`); } } } return Promise.resolve(); } return new Promise((resolve, reject) => { this._socket.emit('setState', id, val, (err) => (err ? reject(new Error(err)) : resolve())); }); } /** * Gets all objects. * * @param update Set to true to retrieve all objects from the server (instead of using the local cache) * @param disableProgressUpdate Don't call onProgress() when done */ getObjects(update, disableProgressUpdate) { if (!this.connected) { return Promise.reject(new Error(NOT_CONNECTED)); } return new Promise((resolve, reject) => { if (!update && this.objects) { resolve(this.objects); } else { this._socket.emit(LegacyConnection.isWeb() ? 'getObjects' : 'getAllObjects', (err, res) => { this.objects = res; disableProgressUpdate && this.onProgress(PROGRESS.OBJECTS_LOADED); err ? reject(new Error(err)) : resolve(this.objects); }); } }); } /** * Gets objects by list of IDs. * * @param list Array of object IDs to retrieve */ getObjectsById(list) { if (!this.connected) { return Promise.reject(new Error(NOT_CONNECTED)); } return new Promise((resolve, reject) => { this._socket.emit('getObjects', list, (err, res) => err ? reject(new Error(err)) : resolve(res)); }); } /** * Called internally. */ _subscribe(isEnable) { if (isEnable && !this.subscribed) { this.subscribed = true; this.autoSubscribes.forEach(id => this._socket.emit('subscribeObjects', id)); // re-subscribe objects Object.keys(this.objectsSubscribes).forEach(id => this._socket.emit('subscribeObjects', id)); // re-subscribe logs this.autoSubscribeLog && this._socket.emit('requireLog', true); // re-subscribe states const ids = Object.keys(this.statesSubscribes); ids.forEach(id => this._socket.emit('subscribe', id)); ids.length && this._socket.emit(LegacyConnection.isWeb() ? 'getStates' : 'getForeignStates', ids, (err, states) => { err && console.error(`Cannot getForeignStates: ${JSON.stringify(err)}`); // inform about states states && Object.keys(states).forEach(id => this.stateChange(id, states[id])); }); } else if (!isEnable && this.subscribed) { this.subscribed = false; // un-subscribe objects this.autoSubscribes.forEach(id => this._socket.emit('unsubscribeObjects', id)); Object.keys(this.objectsSubscribes).forEach(id => this._socket.emit('unsubscribeObjects', id)); // un-subscribe logs this.autoSubscribeLog && this._socket.emit('requireLog', false); // un-subscribe states Object.keys(this.statesSubscribes).forEach(id => this._socket.emit('unsubscribe', id)); } } /** * Requests log updates. * * @param isEnabled Set to true to get logs */ requireLog(isEnabled) { if (!this.connected) { return Promise.reject(new Error(NOT_CONNECTED)); } return new Promise((resolve, reject) => { this._socket.emit('requireLog', isEnabled, (err) => err ? reject(new Error(err)) : resolve()); }); } /** * Deletes the given object. * * @param id The object ID * @param maintenance Force deletion of non-conform IDs */ delObject(id, maintenance) { if (!this.connected) { return Promise.reject(new Error(NOT_CONNECTED)); } return new Promise((resolve, reject) => { this._socket.emit('delObject', id, { maintenance: !!maintenance }, (err) => err ? reject(new Error(err)) : resolve()); }); } /** * Deletes the given object and all its children. * * @param id The object ID * @param maintenance Force deletion of non-conform IDs */ delObjects(id, maintenance) { if (!this.connected) { return Promise.reject(new Error(NOT_CONNECTED)); } return new Promise((resolve, reject) => { this._socket.emit('delObjects', id, { maintenance: !!maintenance }, (err) => err ? reject(new Error(err)) : resolve()); }); } /** * Sets the object. */ setObject(id, obj) { if (!this.connected) { return Promise.reject(new Error(NOT_CONNECTED)); } if (!obj) { return Promise.reject(new Error('Null object is not allowed')); } obj = JSON.parse(JSON.stringify(obj)); if (Object.prototype.hasOwnProperty.call(obj, 'from')) { delete obj.from; } if (Object.prototype.hasOwnProperty.call(obj, 'user')) { delete obj.user; } if (Object.prototype.hasOwnProperty.call(obj, 'ts')) { delete obj.ts; } return new Promise((resolve, reject) => { this._socket.emit('setObject', id, obj, (err) => (err ? reject(new Error(err)) : resolve())); }); } /** * Gets the object with the given id from the server. */ getObject(id) { if (!this.connected) { return Promise.reject(new Error(NOT_CONNECTED)); } if (id && id === this.ignoreState) { return Promise.resolve({ _id: this.ignoreState, type: 'state', common: { name: 'ignored state', type: 'mixed', read: true, write: true, role: 'state', }, native: {}, }); } return new Promise((resolve, reject) => { this._socket.emit('getObject', id, (err, obj) => err ? reject(new Error(err)) : resolve(obj)); }); } /** * Get all instances of the given adapter or all instances of all adapters. * * @param adapter The name of the adapter * @param update Force update */ getAdapterInstances(adapter, update) { if (typeof adapter === 'boolean') { update = adapter; adapter = ''; } adapter ||= ''; if (!update && this._promises[`instances_${adapter}`] instanceof Promise) { return this._promises[`instances_${adapter}`]; } if (!this.connected) { return Promise.reject(new Error(NOT_CONNECTED)); } this._promises[`instances_${adapter}`] = new Promise((resolve, reject) => { this._socket.emit('getAdapterInstances', adapter, (err, instances) => err ? reject(new Error(err)) : resolve(instances)); }); return this._promises[`instances_${adapter}`]; } /** * Get adapters with the given name or all adapters. * * @param adapter The name of the adapter * @param update Force update */ getAdapters(adapter, update) { if (LegacyConnection.isWeb()) { return Promise.reject(new Error('Allowed only in admin')); } if (typeof adapter === 'boolean') { update = adapter; adapter = ''; } adapter ||= ''; if (!update && this._promises[`adapter_${adapter}`] instanceof Promise) { return this._promises[`adapter_${adapter}`]; } if (!this.connected) { return Promise.reject(new Error(NOT_CONNECTED)); } this._promises[`adapter_${adapter}`] = new Promise((resolve, reject) => { this._socket.emit('getAdapters', adapter, (err, adapters) => { err ? reject(new Error(err)) : resolve(adapters); }); }); return this._promises[`adapter_${adapter}`]; } /** * Called internally. */ _renameGroups(objs, cb) { if (!objs?.length) { cb?.(null); } else { const obj = objs.pop(); if (!obj) { setTimeout(() => this._renameGroups(objs, cb), 0); return; } const oldId = obj._id; obj._id = obj.newId; delete obj.newId; this.setObject(obj._id, obj) .then(() => this.delObject(oldId)) .then(() => setTimeout(() => this._renameGroups(objs, cb), 0)) .catch((err) => cb?.(err)); } } /** * Rename a group. * * @param id The id. * @param newId The new id. * @param newName The new name. */ async renameGroup(id, newId, newName) { if (LegacyConnection.isWeb()) { return Promise.reject(new Error('Allowed only in admin')); } const groups = await this.getGroups(true); if (groups.length) { // find all elements const groupsToRename = groups.filter(group => group._id.startsWith(`${id}.`)); groupsToRename.forEach(group => { group.newId = (newId + group._id.substring(id.length)); }); await new Promise((resolve, reject) => { this._renameGroups(groupsToRename, (err) => err ? reject(new Error(err)) : resolve(null)); }); const obj = groups.find(group => group._id === id); if (obj) { obj._id = newId; if (newName !== undefined) { obj.common ||= {}; obj.common.name = newName; } return this.setObject(obj._id, obj).then(() => this.delObject(id)); } } return Promise.resolve(); } /** * Sends a message to a specific instance or all instances of some specific adapter. * * @param instance The instance to send this message to. * @param command Command name of the target instance. * @param data The message data to send. */ sendTo(instance, command, data) { if (!this.connected) { return Promise.reject(new Error(NOT_CONNECTED)); } return new Promise(resolve => { this._socket.emit('sendTo', instance, command, data, (result) => resolve(result)); }); } /** * Extend an object and create it if it might not exist. * * @param id The id. * @param obj The object. */ extendObject(id, obj) { if (!this.connected) { return Promise.reject(new Error(NOT_CONNECTED)); } obj = JSON.parse(JSON.stringify(obj)); if (Object.prototype.hasOwnProperty.call(obj, 'from')) { delete obj.from; } if (Object.prototype.hasOwnProperty.call(obj, 'user')) { delete obj.user; } if (Object.prototype.hasOwnProperty.call(obj, 'ts')) { delete obj.ts; } return new Promise((resolve, reject) => { this._socket.emit('extendObject', id, obj, (err) => err ? reject(new Error(err)) : resolve()); }); } /** * Register a handler for log messages. */ registerLogHandler(handler) { !this.onLogHandlers.includes(handler) && this.onLogHandlers.push(handler); } /** * Unregister a handler for log messages. */ unregisterLogHandler(handler) { const pos = this.onLogHandlers.indexOf(handler); pos !== -1 && this.onLogHandlers.splice(pos, 1); } /** * Register a handler for the connection state. */ registerConnectionHandler(handler) { !this.onConnectionHandlers.includes(handler) && this.onConnectionHandlers.push(handler); } /** * Unregister a handler for the connection state. */ unregisterConnectionHandler(handler) { const pos = this.onConnectionHandlers.indexOf(handler); pos !== -1 && this.onConnectionHandlers.splice(pos, 1); } /** * Set the handler for standard output of a command. * * @param handler The handler. */ registerCmdStdoutHandler(handler) { this.onCmdStdoutHandler = handler; } /** * Unset the handler for standard output of a command. */ unregisterCmdStdoutHandler( /* handler */) { this.onCmdStdoutHandler = undefined; } /** * Set the handler for standard error of a command. * * @param handler The handler. */ registerCmdStderrHandler(handler) { this.onCmdStderrHandler = handler; } /** * Unset the handler for standard error of a command. */ unregisterCmdStderrHandler() { this.onCmdStderrHandler = undefined; } /** * Set the handler for exit of a command. */ registerCmdExitHandler(handler) { this.onCmdExitHandler = handler; } /** * Unset the handler for exit of a command. */ unregisterCmdExitHandler() { this.onCmdExitHandler = undefined; } /** * Get all enums with the given name. */ getEnums( /** The name of the enum. */ _enum, /** Force update. */ update) { if (!update && this._promises[`enums_${_enum || 'all'}`] instanceof Promise) { return this._promises[`enums_${_enum || 'all'}`]; } if (!this.connected) { return Promise.reject(new Error(NOT_CONNECTED)); } this._promises[`enums_${_enum || 'all'}`] = new Promise((resolve, reject) => { this._socket.emit('getObjectView', 'system', 'enum', { startkey: `enum.${_enum || ''}`, endkey: `enum.${_enum ? `${_enum}.` : ''}\u9999` }, (err, res) => { if (!err && res) { const _res = {}; for (let i = 0; i < res.rows.length; i++) { if (_enum && res.rows[i].id === `enum.${_enum}`) { continue; } _res[res.rows[i].id] = res.rows[i].value; } resolve(_res); } else if (err) { reject(new Error(err)); } else { reject(new Error('Invalid response while getting enums')); } }); }); return this._promises[`enums_${_enum || 'all'}`]; } /** * Query a predefined object view. * * @param design design - 'system' or other designs like `custom`. * @param type The type of object. * @param start The start ID. * @param end The end ID. */ getObjectViewCustom( /** The design: 'system' or other designs like `custom`. */ design, /** The type of object. */ type, /** The start ID. */ start, /** The end ID. */ end) { return new Promise((resolve, reject) => { this._socket.emit('getObjectView', design, type, { startkey: start, endkey: end }, (err, res) => { if (!err) { const _res = {}; if (res && res.rows) { for (let i = 0; i < res.rows.length; i++) { _res[res.rows[i].id] = res.rows[i].value; }