Skip to content

Commit b159ac2

Browse files
[feat]: Added possibility to store additional data with notifications (#2878)
* * (bluefox) Added the link button in notifications * Extended io-package.json schema * Extend link definition * Fixed ip-package.json schema * Added script to run npm on the very start * Added support for notification GUI * Removed link from notifications * Removed link from notifications * Renamed offlineMessage back to message * Added comments * Small updates * Rename actionData to contextData * Changed comment * Cleanup context data notifications (#2904) * prevent having too many args on public methods in the future by introducing options objects * fix jsdoc * added notification and made structure a bit more clear * fix types --------- Co-authored-by: Max Hauser <[email protected]>
1 parent 4bc4069 commit b159ac2

File tree

11 files changed

+221
-144
lines changed

11 files changed

+221
-144
lines changed

CHANGELOG.md

Lines changed: 58 additions & 58 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,12 @@ This method takes the following parameters:
512512
* scope: scope to be addressed
513513
* category: category to be addressed, if a null message will be checked by regex of given scope
514514
* message: message to be stored/checked
515+
* options: Available with js-controller version 6.1. Additional options for the notification, currently you can provide additional `contextData` which is also stored with the notification information. Notification processing adapters can use this data
516+
517+
Note, that the structure of the `contextData` which can be stored via the options object is not defined by the controller. Adapters which handle messages can use individual data attributes.
518+
Currently, it is planned to support individual notification customization in the `admin` adapter. More information will be available in the `admin` adapter as soon as this feature is ready.
519+
520+
As a best practice the top-level of `contextData` should not be populated with individual data belonging to instances. Use a `key` specific to the adapter or if a feature is supported by all adapters of a type, the type (e.g. `messaging`) is also fine.
515521

516522
When a regex is defined then `console.error` output from the adapter is always checked by the regex and notifications are registered automatically when the regex matches!
517523

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"build:ts": "lerna run build --ignore '@iobroker/types'",
8383
"build:types": "npm run build --workspace=@iobroker/types",
8484
"build": "npm run build:ts && npm run build:types",
85+
"npm": "npm i --ignore-scripts",
8586
"postbuild": "npm run update-schema",
8687
"preinstall": "lerna run preinstall",
8788
"install": "lerna run install",

packages/adapter/src/lib/_Types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,3 +598,13 @@ export interface InternalInstallNodeModuleOptions extends InstallNodeModuleOptio
598598
/** Name of the npm module or an installable url ẁorking with `npm install` */
599599
moduleNameOrUrl: string;
600600
}
601+
602+
/**
603+
* Options for the generated notification
604+
*/
605+
export interface NotificationOptions {
606+
/**
607+
* Additional context for the notification which can be used by notification processing adapters
608+
*/
609+
contextData: ioBroker.NotificationContextData;
610+
}

packages/adapter/src/lib/adapter/adapter.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ import type {
132132
InstallNodeModuleOptions,
133133
InternalInstallNodeModuleOptions,
134134
StopParameters,
135-
InternalStopParameters
135+
InternalStopParameters,
136+
NotificationOptions
136137
} from '@/lib/_Types.js';
137138
import { UserInterfaceMessagingController } from '@/lib/adapter/userInterfaceMessagingController.js';
138139
import { SYSTEM_ADAPTER_PREFIX } from '@iobroker/js-controller-common-db/constants';
@@ -7589,17 +7590,19 @@ export class AdapterClass extends EventEmitter {
75897590
registerNotification<Scope extends keyof ioBroker.NotificationScopes>(
75907591
scope: Scope,
75917592
category: ioBroker.NotificationScopes[Scope] | null,
7592-
message: string
7593+
message: string,
7594+
options?: NotificationOptions
75937595
): Promise<void>;
75947596

75957597
/**
75967598
* Send notification with given scope and category to host of this adapter
75977599
*
75987600
* @param scope - scope to be addressed
7599-
* @param category - to be addressed, if null message will be checked by regex of given scope
7601+
* @param category - to be addressed, if a null message will be checked by regex of given scope
76007602
* @param message - message to be stored/checked
7603+
* @param options - Additional options for the notification, currently `contextData` is supported
76017604
*/
7602-
async registerNotification(scope: unknown, category: unknown, message: unknown): Promise<void> {
7605+
async registerNotification(scope: unknown, category: unknown, message: unknown, options?: unknown): Promise<void> {
76037606
if (!this.#states) {
76047607
// if states is no longer existing, we do not need to set
76057608
this._logger.info(
@@ -7614,9 +7617,19 @@ export class AdapterClass extends EventEmitter {
76147617
}
76157618
Validator.assertString(message, 'message');
76167619

7620+
if (options !== undefined) {
7621+
Validator.assertObject<NotificationOptions>(options, 'options');
7622+
}
7623+
76177624
const obj = {
76187625
command: 'addNotification',
7619-
message: { scope, category, message, instance: this.namespace },
7626+
message: {
7627+
scope,
7628+
category,
7629+
message,
7630+
instance: this.namespace,
7631+
contextData: options?.contextData
7632+
},
76207633
from: `system.adapter.${this.namespace}`
76217634
};
76227635

packages/cli/src/lib/setup.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -755,12 +755,12 @@ async function processCommand(
755755
);
756756
await notificationHandler.addConfig(ioPackage.notifications);
757757

758-
await notificationHandler.addMessage(
759-
'system',
760-
'fileToJsonl',
761-
`Migrated: ${migrated}`,
762-
`system.host.${hostname}`
763-
);
758+
await notificationHandler.addMessage({
759+
scope: 'system',
760+
category: 'fileToJsonl',
761+
message: `Migrated: ${migrated}`,
762+
instance: `system.host.${hostname}`
763+
});
764764

765765
notificationHandler.storeNotifications();
766766
} catch (e) {

packages/common/src/lib/common/notificationHandler.ts

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export type Severity = 'info' | 'notify' | 'alert';
3636
export interface CategoryConfigEntry {
3737
category: string;
3838
name: MultilingualObject;
39-
/** `info` will only be shown by admin, while `notify` might also be used by messaging adapters, `alert` ensures both */
39+
/** Allows defining the severity of the notification with `info` being the lowest, `notify` representing middle priority, `alert` representing high priority and often containing critical information */
4040
severity: Severity;
4141
description: MultilingualObject;
4242
regex: string[];
@@ -46,6 +46,7 @@ export interface CategoryConfigEntry {
4646
interface NotificationMessageObject {
4747
message: string;
4848
ts: number;
49+
contextData?: ioBroker.NotificationContextData;
4950
}
5051

5152
interface NotificationsObject {
@@ -99,6 +100,19 @@ interface ScopeStateValue {
99100
};
100101
}
101102

103+
interface AddMessageOptions {
104+
/** Scope of the message */
105+
scope: string;
106+
/** Category of the message, if non we check against regex of scope */
107+
category?: string | null;
108+
/** Message to add */
109+
message: string;
110+
/** Instance e.g., hm-rpc.1 or hostname, if hostname it needs to be prefixed like system.host.rpi */
111+
instance: string;
112+
/** Additional context for the notification which can be used by notification processing adapters */
113+
contextData?: ioBroker.NotificationContextData;
114+
}
115+
102116
export class NotificationHandler {
103117
private states: StatesInRedisClient;
104118
private objects: ObjectsInRedisClient;
@@ -128,14 +142,14 @@ export class NotificationHandler {
128142
// create the initial notifications object
129143
let obj;
130144
try {
131-
obj = await this.objects.getObjectAsync(`system.host.${this.host}.notifications`);
145+
obj = await this.objects.getObject(`system.host.${this.host}.notifications`);
132146
} catch {
133147
// ignore
134148
}
135149

136150
if (!obj) {
137151
try {
138-
await this.objects.setObjectAsync(`system.host.${this.host}.notifications`, {
152+
await this.objects.setObject(`system.host.${this.host}.notifications`, {
139153
type: 'folder',
140154
common: {
141155
name: {
@@ -168,7 +182,7 @@ export class NotificationHandler {
168182
});
169183

170184
for (const entry of res.rows) {
171-
// check that instance has notifications settings
185+
// check that instance has notification settings
172186
if (entry.value.notifications) {
173187
await this.addConfig(entry.value.notifications);
174188
}
@@ -202,7 +216,7 @@ export class NotificationHandler {
202216
/**
203217
* Add a new category to the given scope with a provided optional list of regex
204218
*
205-
* @param notifications - notifications array
219+
* @param notifications - Array with notifications
206220
*/
207221
async addConfig(notifications: NotificationsConfigEntry[]): Promise<void> {
208222
// if valid attributes, store it
@@ -211,14 +225,14 @@ export class NotificationHandler {
211225
// create the state object for each scope if non-existing
212226
let obj;
213227
try {
214-
obj = await this.objects.getObjectAsync(`system.host.${this.host}.notifications.${scopeObj.scope}`);
228+
obj = await this.objects.getObject(`system.host.${this.host}.notifications.${scopeObj.scope}`);
215229
} catch {
216230
// ignore
217231
}
218232

219233
if (!obj) {
220234
try {
221-
await this.objects.setObjectAsync(`system.host.${this.host}.notifications.${scopeObj.scope}`, {
235+
await this.objects.setObject(`system.host.${this.host}.notifications.${scopeObj.scope}`, {
222236
type: 'state',
223237
common: {
224238
type: 'object',
@@ -283,17 +297,12 @@ export class NotificationHandler {
283297
/**
284298
* Add a message to the scope and category
285299
*
286-
* @param scope - scope of the message
287-
* @param category - category of the message, if non we check against regex of scope
288-
* @param message - message to add
289-
* @param instance - instance e.g., hm-rpc.1 or hostname, if hostname it needs to be prefixed like system.host.rpi
300+
* @param options The scope, category, message, instance and contextData information
290301
*/
291-
async addMessage(
292-
scope: string,
293-
category: string | null | undefined,
294-
message: string,
295-
instance: string
296-
): Promise<void> {
302+
async addMessage(options: AddMessageOptions): Promise<void> {
303+
const { message, scope, category, contextData } = options;
304+
let { instance } = options;
305+
297306
if (typeof instance !== 'string') {
298307
this.log.error(
299308
`${this.logPrefix} [addMessage] Instance has to be of type "string", got "${typeof instance}"`
@@ -330,7 +339,7 @@ export class NotificationHandler {
330339
this.currentNotifications[scope][_category][instance] || [];
331340

332341
if (!this.setup[scope]?.categories[_category]) {
333-
// no setup for this instance/category combination found - so nothing to add
342+
// no setup for this instance/category combination found - so we have nothing to add
334343
this.log.warn(
335344
`${this.logPrefix} No configuration found for scope "${scope}" and category "${_category}"`
336345
);
@@ -346,7 +355,7 @@ export class NotificationHandler {
346355
}
347356

348357
// add a new element at the beginning
349-
this.currentNotifications[scope][_category][instance].unshift({ message, ts: Date.now() });
358+
this.currentNotifications[scope][_category][instance].unshift({ message, ts: Date.now(), contextData });
350359
}
351360
}
352361

@@ -361,7 +370,7 @@ export class NotificationHandler {
361370

362371
// set updated scope state
363372
try {
364-
await this.states.setStateAsync(`system.host.${this.host}.notifications.${scope}`, {
373+
await this.states.setState(`system.host.${this.host}.notifications.${scope}`, {
365374
val: JSON.stringify(stateVal),
366375
ack: true
367376
});
@@ -423,7 +432,7 @@ export class NotificationHandler {
423432
}
424433

425434
/**
426-
* Load notifications from file
435+
* Load notifications from a file
427436
*/
428437
private _loadNotifications(): void {
429438
try {
@@ -469,7 +478,11 @@ export class NotificationHandler {
469478
continue;
470479
}
471480

472-
res[scope] = { categories: {}, description: this.setup[scope].description, name: this.setup[scope].name };
481+
res[scope] = {
482+
categories: {},
483+
description: this.setup[scope].description,
484+
name: this.setup[scope].name
485+
};
473486

474487
for (const category of Object.keys(this.currentNotifications[scope])) {
475488
if (categoryFilter && categoryFilter !== category) {

0 commit comments

Comments
 (0)