diff --git a/README.md b/README.md index 5a62a0e23..557326390 100644 --- a/README.md +++ b/README.md @@ -424,7 +424,7 @@ The definition also contains localized names and descriptions that can be used t Registered notifications are stored per host and can be requested via messages to the host. They are also stored when the controller stops and loaded on next start. Additionally, a summary of the stored categories per scope with a number of stored notifications and the last added timestamp is available in the state `system.host.hostname.notifications.scopeid` as a JSON. -The js-controller defines in its io-package the system scope together with all details. You can use this as an example and the JSON schema will help you validate your notifications. +The js-controller defines in its io-package the system scope together with all details. You can use this as an example, and the JSON schema will help you validate your notifications. ```json { @@ -534,7 +534,7 @@ The message needs to take the following parameters in the message object: * categoryFilter - category of notifications * instanceFilter - instance of notifications -All three are optional and can be a string or null/undefined if ommited. +All three are optional and can be a string or null/undefined if omitted. ### Disk space warnings **Feature status:** New in 6.0.0 @@ -546,7 +546,7 @@ By default, this threshold is 5 % of disk space. Via the state `system.host. { if (!repoName) { - const sysConfig = systemConfig || (await this.objects.getObjectAsync('system.config')); + const sysConfig = systemConfig || (await this.objects.getObject('system.config')); repoName = sysConfig!.common.activeRepo; } - const oldRepos = systemRepos || (await this.objects.getObjectAsync('system.repositories')); + const oldRepos = systemRepos || (await this.objects.getObject('system.repositories')); if (!oldRepos?.native.repositories?.[repoName]) { console.log(`Error: repository "${repoName}" not found in the "system.repositories`); return null; @@ -101,7 +101,7 @@ export class Repo { (urlOrPath.startsWith('http://') || urlOrPath.startsWith('https://')) ) { try { - hash = await axios({ url: hashUrl, timeout: 10000 }); + hash = await axios({ url: hashUrl, timeout: 10_000 }); } catch (e) { console.error(`Cannot download repository hash file from "${hashUrl}": ${e.message}`); } @@ -161,7 +161,7 @@ export class Repo { if (changed) { oldRepos.from = `system.host.${tools.getHostName()}.cli`; oldRepos.ts = Date.now(); - await this.objects.setObjectAsync('system.repositories', oldRepos); + await this.objects.setObject('system.repositories', oldRepos); } return oldRepos.native.repositories[repoName].json; @@ -319,8 +319,8 @@ export class Repo { const listStr = list.join(', '); for (const row of objs.rows) { if (row?.value?.type === 'instance') { - await this.states.setStateAsync(`${row.id}.info.updatesNumber`, { val: list.length, ack: true }); - await this.states.setStateAsync(`${row.id}.info.updatesList`, { val: listStr, ack: true }); + await this.states.setState(`${row.id}.info.updatesNumber`, { val: list.length, ack: true }); + await this.states.setState(`${row.id}.info.updatesList`, { val: listStr, ack: true }); } } } @@ -412,7 +412,7 @@ export class Repo { delete repoObj.native.repositories[repoName]; repoObj.from = `system.host.${tools.getHostName()}.cli`; repoObj.ts = Date.now(); - await this.objects.setObjectAsync('system.repositories', repoObj); + await this.objects.setObject('system.repositories', repoObj); } } } diff --git a/packages/controller/src/main.ts b/packages/controller/src/main.ts index ffd7a7916..3014b30d3 100644 --- a/packages/controller/src/main.ts +++ b/packages/controller/src/main.ts @@ -137,6 +137,9 @@ interface RepoRequester { callback: ioBroker.MessageCallbackInfo; } +/** Host information including host id and running version */ +type HostInformation = ioBroker.HostCommon & { host: string; runningVersion: string }; + const VIS_ADAPTERS = ['vis', 'vis-2'] as const; const ioPackage = fs.readJSONSync(path.join(tools.getControllerDir(), 'io-package.json')); const version = ioPackage.common.version; @@ -1280,7 +1283,6 @@ function cleanAutoSubscribes(instanceID: ioBroker.ObjectIDs.Instance, callback: // remove this instance from autoSubscribe if (row.value?.common.subscribable) { count++; - // @ts-expect-error https://github.com/ioBroker/ioBroker.js-controller/issues/2089 cleanAutoSubscribe(instance, row.id, () => !--count && callback && callback()); } } @@ -1832,7 +1834,7 @@ function initMessageQueue(): void { } /** - * Send a message to other adapter instance + * Send a message to another adapter instance * * @param objName - adapter name (hm-rpc) or id like system.host.rpi/system.adapter,hm-rpc * @param command @@ -1891,10 +1893,11 @@ async function sendTo( } /** + * Get the version information from given host * - * @param hostId + * @param hostId host to get the version information from */ -async function getVersionFromHost(hostId: ioBroker.ObjectIDs.Host): Promise | null | undefined> { +async function getVersionFromHost(hostId: ioBroker.ObjectIDs.Host): Promise { const state = await states!.getState(`${hostId}.alive`); if (state?.val) { return new Promise(resolve => { @@ -1908,6 +1911,7 @@ async function getVersionFromHost(hostId: ioBroker.ObjectIDs.Host): Promise { if (!uploadTasks.length) { return; @@ -2211,40 +2218,34 @@ async function processMessage(msg: ioBroker.SendableMessage): Promise { - const result: Record = tools.getInstalledInfo(version); - result.hosts = {}; - if (doc?.rows.length) { - // Read installed versions of all hosts - for (const row of doc.rows) { - // If desired a local version, do not ask it, just answer - if (row.id === hostObjectPrefix) { - const ioPackCommon = deepClone(ioPackage.common); - - ioPackCommon.host = hostname; - ioPackCommon.runningVersion = version; - result.hosts[hostname] = ioPackCommon; - } else { - // @ts-expect-error https://github.com/ioBroker/ioBroker.js-controller/issues/2089 - const ioPack = await getVersionFromHost(row.id); - if (ioPack) { - result.hosts[ioPack.host] = ioPack; - result.hosts[ioPack.host].controller = true; - } - } + const doc = await objects!.getObjectViewAsync('system', 'host', { + startkey: 'system.host.', + endkey: 'system.host.\u9999' + }); + + const installedInfo = tools.getInstalledInfo(); + const hosts: Record = {}; + + if (doc?.rows.length) { + // Read installed versions of all hosts + for (const row of doc.rows) { + // If desired a local version, do not ask it, just answer + if (row.id === hostObjectPrefix) { + const ioPackCommon = deepClone(ioPackage.common); + + ioPackCommon.host = hostname; + ioPackCommon.runningVersion = version; + hosts[hostname] = ioPackCommon; + } else { + const ioPack = await getVersionFromHost(row.id); + if (ioPack) { + hosts[ioPack.host] = ioPack; } } - - sendTo(msg.from, msg.command, result, msg.callback); } - ); + } + + sendTo(msg.from, msg.command, { ...installedInfo, hosts }, msg.callback); } else { logger.error(`${hostLogPrefix} Invalid request ${msg.command}. "callback" or "from" is null`); } @@ -2270,7 +2271,9 @@ async function processMessage(msg: ioBroker.SendableMessage): Promise { if (!objects) { throw new Error('Objects database not connected'); diff --git a/packages/types-dev/index.d.ts b/packages/types-dev/index.d.ts index 7b4767d74..403e674d4 100644 --- a/packages/types-dev/index.d.ts +++ b/packages/types-dev/index.d.ts @@ -478,7 +478,7 @@ declare global { interface GetObjectViewItem { /** The ID of this object */ - id: string; + id: T['_id']; /** A copy of the object from the DB */ value: T; } diff --git a/packages/types-dev/objects.d.ts b/packages/types-dev/objects.d.ts index f855c44df..d1676de50 100644 --- a/packages/types-dev/objects.d.ts +++ b/packages/types-dev/objects.d.ts @@ -595,6 +595,9 @@ declare global { description?: ioBroker.StringOrTranslated; }; + /** Format for local and global dependencies */ + type Depdendencies = { [adapterName: string]: string }[] | string[]; + interface AdapterCommon extends ObjectCommon { /** Custom attributes to be shown in admin in the object browser */ adminColumns?: string | (string | CustomAdminColumn)[]; @@ -632,9 +635,9 @@ declare global { /** How the adapter will mainly receive its data. Set this together with @see connectionType */ dataSource?: 'poll' | 'push' | 'assumption'; /** A record of ioBroker adapters (including "js-controller") and version ranges which are required for this adapter on the same host. */ - dependencies?: Array>; + dependencies?: Depdendencies; /** A record of ioBroker adapters (including "js-controller") and version ranges which are required for this adapter in the whole system. */ - globalDependencies?: Array>; + globalDependencies?: Depdendencies; /** Which files outside the README.md have documentation for the adapter */ docs?: Partial>; /** Whether new instances should be enabled by default. *Should* be `false`! */ @@ -822,6 +825,8 @@ declare global { defaultLogLevel?: LogLevel; /** Used date format for formatting */ dateFormat: string; + /** This name will be shown in admin's header. Just to identify the whole installation */ + siteName?: string; /** Default acl for new objects */ defaultNewAcl: { object: number; @@ -865,9 +870,9 @@ declare global { } /** - * ioBroker has built-in protection for specific attributes of objects. If this protection is installed in the object, then the protected attributes of object cannot be changed by the user without valid password. + * ioBroker has built-in protection for specific attributes of objects. If this protection is installed in the object, then the protected attributes of an object cannot be changed by the user without a valid password. * To protect the properties from change, the special attribute "nonEdit" must be added to the object. This attribute contains the password, which is required to change the object. - * If object does not have "nonEdit" attribute, so the hash will be saved into "nonEdit.passHash". After that if someone will change the object, he must provide the password in "nonEdit.password". + * If an object does not have "nonEdit" attribute, so the hash will be saved into "nonEdit.passHash". After that, if someone changes the object, he must provide the password in "nonEdit.password". * If the password is correct, the object attributes will be updated. If the password is wrong, the object will not be changed. * Note, that all properties outside "nonEdit" can be updated without providing the password. Furthermore, do not confuse e.g. "nonEdit.common" with "obj.common" they are not linked in any way. */ @@ -1012,6 +1017,8 @@ declare global { json: RepositoryJson | null; hash?: string; time?: string; + /** If this repository stable */ + stable?: boolean; } interface RepositoryObject extends BaseObject { @@ -1048,13 +1055,47 @@ declare global { common?: Partial; } - /** TODO: To be defined */ - type NotificationCategory = any; + // it is defined in notificationHandler.ts + type NotificationCategory = { + /** The unique category identifier */ + category: + | 'memIssues' + | 'fsIoErrors' + | 'noDiskSpace' + | 'accessErrors' + | 'nonExistingFileErrors' + | 'remoteHostErrors' + | 'restartLoop' + | 'fileToJsonl' + | 'automaticAdapterUpgradeFailed' + | 'automaticAdapterUpgradeSuccessful' + | 'blockedVersions' + | 'databaseErrors' + | 'securityIssues' + | 'packageUpdates' + | 'systemRebootRequired' + | 'diskSpaceIssues' + | string; + /** The human-readable category name */ + name: Translated; + /** The human-readable category description */ + description: Translated; + /** Allows to define the severity of the notification with `info` being the lowest `notify` representing middle priority, `alert` representing high priority and often containing critical information */ + severity: 'info' | 'notify' | 'alert'; + /** If a regex is specified, the js-controller will check error messages on adapter crashes against this regex and will generate a notification of this category */ + regex: string[]; + /** Deletes older messages if more than the specified amount is present for this category */ + limit: number; + }; interface Notification { + /** Each adapter can define its own "scopes" for own notifications with its own categories which then will be available in the system. Adapters should only register one scope which matches the name of the adapter. */ scope: string; + /** The human-readable name of this scope */ name: Translated; + /** The human-readable description of this scope */ description: Translated; + /** All notification categories of this scope */ categories: NotificationCategory[]; }