Skip to content

Commit

Permalink
[feat]: Added possibility to store additional data with notifications (
Browse files Browse the repository at this point in the history
…#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]>
  • Loading branch information
GermanBluefox and foxriver76 authored Sep 10, 2024
1 parent 4bc4069 commit b159ac2
Show file tree
Hide file tree
Showing 11 changed files with 221 additions and 144 deletions.
116 changes: 58 additions & 58 deletions CHANGELOG.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,12 @@ This method takes the following parameters:
* scope: scope to be addressed
* category: category to be addressed, if a null message will be checked by regex of given scope
* message: message to be stored/checked
* 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

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.
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.

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.

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!

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"build:ts": "lerna run build --ignore '@iobroker/types'",
"build:types": "npm run build --workspace=@iobroker/types",
"build": "npm run build:ts && npm run build:types",
"npm": "npm i --ignore-scripts",
"postbuild": "npm run update-schema",
"preinstall": "lerna run preinstall",
"install": "lerna run install",
Expand Down
10 changes: 10 additions & 0 deletions packages/adapter/src/lib/_Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,3 +598,13 @@ export interface InternalInstallNodeModuleOptions extends InstallNodeModuleOptio
/** Name of the npm module or an installable url ẁorking with `npm install` */
moduleNameOrUrl: string;
}

/**
* Options for the generated notification
*/
export interface NotificationOptions {
/**
* Additional context for the notification which can be used by notification processing adapters
*/
contextData: ioBroker.NotificationContextData;
}
23 changes: 18 additions & 5 deletions packages/adapter/src/lib/adapter/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ import type {
InstallNodeModuleOptions,
InternalInstallNodeModuleOptions,
StopParameters,
InternalStopParameters
InternalStopParameters,
NotificationOptions
} from '@/lib/_Types.js';
import { UserInterfaceMessagingController } from '@/lib/adapter/userInterfaceMessagingController.js';
import { SYSTEM_ADAPTER_PREFIX } from '@iobroker/js-controller-common-db/constants';
Expand Down Expand Up @@ -7589,17 +7590,19 @@ export class AdapterClass extends EventEmitter {
registerNotification<Scope extends keyof ioBroker.NotificationScopes>(
scope: Scope,
category: ioBroker.NotificationScopes[Scope] | null,
message: string
message: string,
options?: NotificationOptions
): Promise<void>;

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

if (options !== undefined) {
Validator.assertObject<NotificationOptions>(options, 'options');
}

const obj = {
command: 'addNotification',
message: { scope, category, message, instance: this.namespace },
message: {
scope,
category,
message,
instance: this.namespace,
contextData: options?.contextData
},
from: `system.adapter.${this.namespace}`
};

Expand Down
12 changes: 6 additions & 6 deletions packages/cli/src/lib/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,12 +755,12 @@ async function processCommand(
);
await notificationHandler.addConfig(ioPackage.notifications);

await notificationHandler.addMessage(
'system',
'fileToJsonl',
`Migrated: ${migrated}`,
`system.host.${hostname}`
);
await notificationHandler.addMessage({
scope: 'system',
category: 'fileToJsonl',
message: `Migrated: ${migrated}`,
instance: `system.host.${hostname}`
});

notificationHandler.storeNotifications();
} catch (e) {
Expand Down
57 changes: 35 additions & 22 deletions packages/common/src/lib/common/notificationHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export type Severity = 'info' | 'notify' | 'alert';
export interface CategoryConfigEntry {
category: string;
name: MultilingualObject;
/** `info` will only be shown by admin, while `notify` might also be used by messaging adapters, `alert` ensures both */
/** 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 */
severity: Severity;
description: MultilingualObject;
regex: string[];
Expand All @@ -46,6 +46,7 @@ export interface CategoryConfigEntry {
interface NotificationMessageObject {
message: string;
ts: number;
contextData?: ioBroker.NotificationContextData;
}

interface NotificationsObject {
Expand Down Expand Up @@ -99,6 +100,19 @@ interface ScopeStateValue {
};
}

interface AddMessageOptions {
/** Scope of the message */
scope: string;
/** Category of the message, if non we check against regex of scope */
category?: string | null;
/** Message to add */
message: string;
/** Instance e.g., hm-rpc.1 or hostname, if hostname it needs to be prefixed like system.host.rpi */
instance: string;
/** Additional context for the notification which can be used by notification processing adapters */
contextData?: ioBroker.NotificationContextData;
}

export class NotificationHandler {
private states: StatesInRedisClient;
private objects: ObjectsInRedisClient;
Expand Down Expand Up @@ -128,14 +142,14 @@ export class NotificationHandler {
// create the initial notifications object
let obj;
try {
obj = await this.objects.getObjectAsync(`system.host.${this.host}.notifications`);
obj = await this.objects.getObject(`system.host.${this.host}.notifications`);
} catch {
// ignore
}

if (!obj) {
try {
await this.objects.setObjectAsync(`system.host.${this.host}.notifications`, {
await this.objects.setObject(`system.host.${this.host}.notifications`, {
type: 'folder',
common: {
name: {
Expand Down Expand Up @@ -168,7 +182,7 @@ export class NotificationHandler {
});

for (const entry of res.rows) {
// check that instance has notifications settings
// check that instance has notification settings
if (entry.value.notifications) {
await this.addConfig(entry.value.notifications);
}
Expand Down Expand Up @@ -202,7 +216,7 @@ export class NotificationHandler {
/**
* Add a new category to the given scope with a provided optional list of regex
*
* @param notifications - notifications array
* @param notifications - Array with notifications
*/
async addConfig(notifications: NotificationsConfigEntry[]): Promise<void> {
// if valid attributes, store it
Expand All @@ -211,14 +225,14 @@ export class NotificationHandler {
// create the state object for each scope if non-existing
let obj;
try {
obj = await this.objects.getObjectAsync(`system.host.${this.host}.notifications.${scopeObj.scope}`);
obj = await this.objects.getObject(`system.host.${this.host}.notifications.${scopeObj.scope}`);
} catch {
// ignore
}

if (!obj) {
try {
await this.objects.setObjectAsync(`system.host.${this.host}.notifications.${scopeObj.scope}`, {
await this.objects.setObject(`system.host.${this.host}.notifications.${scopeObj.scope}`, {
type: 'state',
common: {
type: 'object',
Expand Down Expand Up @@ -283,17 +297,12 @@ export class NotificationHandler {
/**
* Add a message to the scope and category
*
* @param scope - scope of the message
* @param category - category of the message, if non we check against regex of scope
* @param message - message to add
* @param instance - instance e.g., hm-rpc.1 or hostname, if hostname it needs to be prefixed like system.host.rpi
* @param options The scope, category, message, instance and contextData information
*/
async addMessage(
scope: string,
category: string | null | undefined,
message: string,
instance: string
): Promise<void> {
async addMessage(options: AddMessageOptions): Promise<void> {
const { message, scope, category, contextData } = options;
let { instance } = options;

if (typeof instance !== 'string') {
this.log.error(
`${this.logPrefix} [addMessage] Instance has to be of type "string", got "${typeof instance}"`
Expand Down Expand Up @@ -330,7 +339,7 @@ export class NotificationHandler {
this.currentNotifications[scope][_category][instance] || [];

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

// add a new element at the beginning
this.currentNotifications[scope][_category][instance].unshift({ message, ts: Date.now() });
this.currentNotifications[scope][_category][instance].unshift({ message, ts: Date.now(), contextData });
}
}

Expand All @@ -361,7 +370,7 @@ export class NotificationHandler {

// set updated scope state
try {
await this.states.setStateAsync(`system.host.${this.host}.notifications.${scope}`, {
await this.states.setState(`system.host.${this.host}.notifications.${scope}`, {
val: JSON.stringify(stateVal),
ack: true
});
Expand Down Expand Up @@ -423,7 +432,7 @@ export class NotificationHandler {
}

/**
* Load notifications from file
* Load notifications from a file
*/
private _loadNotifications(): void {
try {
Expand Down Expand Up @@ -469,7 +478,11 @@ export class NotificationHandler {
continue;
}

res[scope] = { categories: {}, description: this.setup[scope].description, name: this.setup[scope].name };
res[scope] = {
categories: {},
description: this.setup[scope].description,
name: this.setup[scope].name
};

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

0 comments on commit b159ac2

Please sign in to comment.