@iobroker/adapter-react-v5
Version:
React components to develop ioBroker interfaces with react.
1,390 lines • 106 kB
JavaScript
/** 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;
}