From 89a1ffc43fd43f51e27e3443fa82fc77b7c0ce68 Mon Sep 17 00:00:00 2001 From: Samuel Weibel Date: Mon, 26 Jan 2026 11:07:42 +0100 Subject: [PATCH 01/23] Fixed name of custom messagebox setting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 375c807..dcb1a6d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Utility classes for ioBroker adapters to support [ioBroker.device-manager](https Add in your `io-package.json` the property `deviceManager: true` to `common.supportedMessages`. Note: If you don't have a `common.supportedMessages` property yet, you have to add it. -Also, if you don't have a `common.supportedMessages.messagebox: true` property yet, you have to add it. If common.messagebox exists, you can remove it. (see +Also, if you don't have a `common.supportedMessages.custom: true` property yet, you have to add it. If common.messagebox exists, you can remove it. (see https://github.com/ioBroker/ioBroker.js-controller/blob/274f9e8f84dbdaaba9830a6cc00ddf083e989090/schemas/io-package.json#L754C104-L754C178) In your ioBroker adapter, add a subclass of `DeviceManagement` and override the methods you need (see next chapters): From 8080e2857707384433ee5826d4dbaec6b253185a Mon Sep 17 00:00:00 2001 From: Samuel Weibel Date: Mon, 26 Jan 2026 11:07:56 +0100 Subject: [PATCH 02/23] Upgrade vscode settings --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index af09d1c..6e92909 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,7 +5,7 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "[typescript]": { "editor.codeActionsOnSave": { - "source.organizeImports": true + "source.organizeImports": "explicit" } } } From e865d217eb552add79a24d9de4e6000a96ba28c9 Mon Sep 17 00:00:00 2001 From: Samuel Weibel Date: Mon, 16 Feb 2026 08:10:33 +0100 Subject: [PATCH 03/23] Add missing ignoreApplyDisabled documentation --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dcb1a6d..cda703e 100644 --- a/README.md +++ b/README.md @@ -268,7 +268,7 @@ This asynchronous method returns (or rather: the Promise is resolved) once the u - `true` if the user clicked "OK" - `false` if the user clicked "Cancel" -### `showForm(schema: JsonFormSchema, options?: { data?: JsonFormData; title?: string })` +### `showForm(schema: JsonFormSchema, options?: { data?: JsonFormData; title?: string; ignoreApplyDisabled?: boolean })` Shows a dialog with a Custom JSON form that can be edited by the user. @@ -277,6 +277,7 @@ The method has the following parameters: - `options` (object, optional): options to configure the dialog further - `data` (object, optional): the data used to populate the Custom JSON form - `title` (string, optional): the dialog title + - `ignoreApplyDisabled` (boolean, optional): set to `true` to always enable the "OK" button even if the form is unchanged This asynchronous method returns (or rather: the Promise is resolved) once the user has clicked a button in the dialog: - the form data, if the user clicked "OK" From 7ce8d5d93eed5a9842411546745e555d36110524 Mon Sep 17 00:00:00 2001 From: Samuel Weibel Date: Mon, 16 Feb 2026 08:24:04 +0100 Subject: [PATCH 04/23] Formatted with prettier --- README.md | 206 ++++++++++++++++++++++++---------------- src/DeviceManagement.ts | 10 +- src/types/index.ts | 2 +- 3 files changed, 128 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index cda703e..add75ca 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Utility classes for ioBroker adapters to support [ioBroker.device-manager](https Add in your `io-package.json` the property `deviceManager: true` to `common.supportedMessages`. Note: If you don't have a `common.supportedMessages` property yet, you have to add it. -Also, if you don't have a `common.supportedMessages.custom: true` property yet, you have to add it. If common.messagebox exists, you can remove it. (see +Also, if you don't have a `common.supportedMessages.custom: true` property yet, you have to add it. If common.messagebox exists, you can remove it. (see https://github.com/ioBroker/ioBroker.js-controller/blob/274f9e8f84dbdaaba9830a6cc00ddf083e989090/schemas/io-package.json#L754C104-L754C178) In your ioBroker adapter, add a subclass of `DeviceManagement` and override the methods you need (see next chapters): @@ -30,7 +30,7 @@ class MyAdapter extends utils.Adapter { public constructor(options: Partial = {}) { super({ ...options, - name: "my-adapter", + name: 'my-adapter', }); this.deviceManagement = new DmTestDeviceManagement(this); @@ -73,6 +73,7 @@ the `DeviceManagement` implementation's `handleXxxAction()` is called, and the a The communication between the `ioBroker.device-manager` tab and the adapter happens through `sendTo`. **IMPORTANT:** make sure your adapter doesn't handle `sendTo` messages starting with `dm:`, otherwise the communication will not work. + - Use for Example this on the top of your onMessage Methode: ```js @@ -88,11 +89,12 @@ You can access all adapter methods like `getState()` or `getStateAsync()` via `t Example: `this.getState()` -> `this.adapter.getState()` ### Error Codes -| Code | Description | -|------|------------------------------------------------------------------------------------------------------------------------------| -| 101 | Instance action ${actionId} was called before getInstanceInfo() was called. This could happen if the instance has restarted. | -| 102 | Instance action ${actionId} is unknown. | -| 103 | Instance action ${actionId} is disabled because it has no handler. | + +| Code | Description | +| ---- | ---------------------------------------------------------------------------------------------------------------------------- | +| 101 | Instance action ${actionId} was called before getInstanceInfo() was called. This could happen if the instance has restarted. | +| 102 | Instance action ${actionId} is unknown. | +| 103 | Instance action ${actionId} is disabled because it has no handler. | | 201 | Device action ${actionId} was called before listDevices() was called. This could happen if the instance has restarted. | | 202 | Device action ${actionId} was called on unknown device: ${deviceId}. | | 203 | Device action ${actionId} doesn't exist on device ${deviceId}. | @@ -101,6 +103,7 @@ Example: `this.getState()` -> `this.adapter.getState()` ## Examples To get an idea of how to use `dm-utils`, please have a look at: + - [the folder "examples"](examples/dm-test.ts) or - [ioBroker.dm-test](https://github.com/UncleSamSwiss/ioBroker.dm-test) @@ -130,10 +133,10 @@ Every array entry is an object of type `DeviceInfo` which has the following prop - `battery` (boolean / number): if boolean: false: Battery empty. If number: battery level of the device (shows also a battery symbol at card) - `warning` (boolean / string): if boolean: true indicates a warning. If string: shows also the warning with mouseover - `actions` (array, optional): an array of actions that can be performed on the device; each object contains: - - `id` (string): unique identifier to recognize an action (never shown to the user) - - `icon` (string): an icon shown on the button (see below for details) - - `description` (string, optional): a text that will be shown as a tooltip on the button - - `disabled` (boolean, optional): if set to `true`, the button can't be clicked but is shown to the user + - `id` (string): unique identifier to recognize an action (never shown to the user) + - `icon` (string): an icon shown on the button (see below for details) + - `description` (string, optional): a text that will be shown as a tooltip on the button + - `disabled` (boolean, optional): if set to `true`, the button can't be clicked but is shown to the user - `hasDetails` (boolean, optional): if set to `true`, the row of the device can be expanded and details are shown below Possible strings for device icons are here: [TYPE ICONS](https://github.com/ioBroker/adapter-react-v5/blob/main/src/Components/DeviceType/DeviceTypeIcon.tsx#L68) @@ -142,6 +145,7 @@ Possible strings for action icons are here: [ACTION NAMES](https://github.com/io
Possible strings for configuration icons are here: [CONFIGURATION TYPES](https://github.com/ioBroker/dm-utils/blob/b3e54ecfaedd6a239beec59c5deb8117d1d59d7f/src/types/common.ts#L110)
+ ### `getInstanceInfo()` This method allows the device manager tab to gather some general information about the instance. It is called when the user opens the tab. @@ -150,11 +154,11 @@ If you override this method, the returned object must contain: - `apiVersion` (string): the supported API version; must be `"v1"` or `"v2"` (if "backend to GUI communication" is used or IDs instead of values) - `actions` (array, optional): an array of actions that can be performed on the instance; each object contains: - - `id` (string): unique identifier to recognize an action (never shown to the user) - - `icon` (string): an icon shown on the button (see below for details) - - `title` (string): the title shown next to the icon on the button - - `description` (string, optional): a text that will be shown as a tooltip on the button - - `disabled` (boolean, optional): if set to `true`, the button can't be clicked but is shown to the user + - `id` (string): unique identifier to recognize an action (never shown to the user) + - `icon` (string): an icon shown on the button (see below for details) + - `title` (string): the title shown next to the icon on the button + - `description` (string, optional): a text that will be shown as a tooltip on the button + - `disabled` (boolean, optional): if set to `true`, the button can't be clicked but is shown to the user - `communicationStateId` (string) (optional): the ID of the state that is used by backend for communication with front-end (only API v2) ### `getDeviceDetails(id: string)` @@ -176,10 +180,12 @@ Please keep in mind that there is no "Save" button, so in most cases, the form s This method is called when to user clicks on an action (i.e., button) for an adapter instance. The parameters of this method are: + - `actionId` (string): the `id` that was given in `getInstanceInfo()` --> `actions[].id` - `context` (object): object containing helper methods that can be used when executing the action The returned object must contain: + - `refresh` (boolean): set this to `true` if you want the list to be reloaded after this action This method can be implemented asynchronously and can take a lot of time to complete. @@ -191,11 +197,13 @@ See below for how to interact with the user. This method is called when the user clicks on an action (i.e., button) for a device. The parameters of this method are: + - `deviceId` (string): the `id` that was given in `listDevices()` --> `[].id` - `actionId` (string): the `id` that was given in `listDevices()` --> `[].actions[].id` - `context` (object): object containing helper methods that can be used when executing the action The returned object must contain: + - `refresh` (string / boolean): the following values are allowed: - `"device"`: if you want the device details to be reloaded after this action - `"instance"`: if you want the entire device list to be reloaded after this action @@ -210,12 +218,14 @@ See below for how to interact with the user. This method is called when the user clicks on a control (i.e., slider) in the device card. The parameters of this method are: + - `deviceId` (string): the `id` that was given in `listDevices()` --> `[].id` - `controlId` (string): the `id` that was given in `listDevices()` --> `[].controls[].id`. There are some reserved control names, you can find the list below. - `state` (string | number | boolean): new state for the control, that will be sent to a real device - `context` (object): object containing helper methods that can be used when executing the action The returned object must contain: + - `state`: ioBroker state object This method can be implemented asynchronously and can take a lot of time to complete. @@ -225,11 +235,13 @@ This method can be implemented asynchronously and can take a lot of time to comp This method is called when GUI requests the update of the state. The parameters of this method are: + - `deviceId` (string): the `id` that was given in `listDevices()` --> `[].id` - `controlId` (string): the `id` that was given in `listDevices()` --> `[].controls[].id` - `context` (object): object containing helper methods that can be used when executing the action The returned object must contain: + - `state`: ioBroker state object This method can be implemented asynchronously and can take a lot of time to complete. @@ -244,6 +256,7 @@ Inside an action method (`handleInstanceAction()` or `handleDeviceAction()`) you For interactions, there are methods you can call on `context`: There are some reserved action names, you can find the list below: + - `status` - This action is called when the user clicks on the status icon. So to implement the "click-on-status" functionality, the developer has to implement this action. - `disable` - This action will be called when the user clicks on the `enabled` icon. `disable` and `enable` actions cannot be together. - `enable` - This action will be called when the user clicks on the `disabled` icon. `disable` and `enable` actions cannot be together. @@ -253,6 +266,7 @@ There are some reserved action names, you can find the list below: Shows a message to the user. The method has the following parameter: + - `text` (string or translation): the text to show to the user This asynchronous method returns (or rather: the Promise is resolved) once the user has clicked on "OK". @@ -262,9 +276,11 @@ This asynchronous method returns (or rather: the Promise is resolved) once the u Lets the user confirm an action by showing a message with an "OK" and "Cancel" button. The method has the following parameter: + - `text` (string or translation): the text to show to the user This asynchronous method returns (or rather: the Promise is resolved) once the user has clicked a button in the dialog: + - `true` if the user clicked "OK" - `false` if the user clicked "Cancel" @@ -273,13 +289,15 @@ This asynchronous method returns (or rather: the Promise is resolved) once the u Shows a dialog with a Custom JSON form that can be edited by the user. The method has the following parameters: + - `schema` (Custom JSON form schema): the schema of the Custom JSON form to show in the dialog - `options` (object, optional): options to configure the dialog further - - `data` (object, optional): the data used to populate the Custom JSON form - - `title` (string, optional): the dialog title - - `ignoreApplyDisabled` (boolean, optional): set to `true` to always enable the "OK" button even if the form is unchanged + - `data` (object, optional): the data used to populate the Custom JSON form + - `title` (string, optional): the dialog title + - `ignoreApplyDisabled` (boolean, optional): set to `true` to always enable the "OK" button even if the form is unchanged This asynchronous method returns (or rather: the Promise is resolved) once the user has clicked a button in the dialog: + - the form data, if the user clicked "OK" - `undefined`, if the user clicked "Cancel" @@ -288,11 +306,12 @@ This asynchronous method returns (or rather: the Promise is resolved) once the u Shows a dialog with a linear progress bar to the user. There is no way for the user to dismiss this dialog. The method has the following parameters: + - `title` (string): the dialog title - `options` (object, optional): options to configure the dialog further - - `indeterminate` (boolean, optional): set to `true` to visualize an unspecified wait time - - `value` (number, optional): the progress value to show to the user (if set, it must be a value between 0 and 100) - - `label` (string, optional): label to show to the right of the progress bar; you may show the progress value in a human-readable way (e.g. "42%") or show the current step in a multi-step progress (e.g. "Logging in...") + - `indeterminate` (boolean, optional): set to `true` to visualize an unspecified wait time + - `value` (number, optional): the progress value to show to the user (if set, it must be a value between 0 and 100) + - `label` (string, optional): label to show to the right of the progress bar; you may show the progress value in a human-readable way (e.g. "42%") or show the current step in a multi-step progress (e.g. "Logging in...") This method returns a promise that resolves to a `ProgressDialog` object. @@ -301,158 +320,177 @@ This method returns a promise that resolves to a `ProgressDialog` object. `ProgressDialog` has two methods: - `update(update: { title?: string; indeterminate?: boolean; value?:number; label?: string; })` - - Updates the progress dialog with new values - - The method has the following parameter: - - `update` (object): what to update in the dialog - - `title` (string, optional): change the dialog title - - `indeterminate` (boolean, optional): change whether the progress is indeterminate - - `value` (number, optional): change the progress value - - `label` (string, optional): change the label to the right of the progress bar + - Updates the progress dialog with new values + - The method has the following parameter: + - `update` (object): what to update in the dialog + - `title` (string, optional): change the dialog title + - `indeterminate` (boolean, optional): change whether the progress is indeterminate + - `value` (number, optional): change the progress value + - `label` (string, optional): change the label to the right of the progress bar - `close()` - - Closes the progress dialog (and allows you to open other dialogs) + - Closes the progress dialog (and allows you to open other dialogs) ### `sendCommandToGui(command: BackendToGuiCommand)` Sends command to GUI to add/update/delete devices or to update the status of device. -**It is suggested** to use the state's ID directly in DeviceInfo structure instead of sending the command every time to GUI on status update. +**It is suggested** to use the state's ID directly in DeviceInfo structure instead of sending the command every time to GUI on status update. See example below: + ```ts class MyAdapterDeviceManagement extends DeviceManagement { - protected listDevices(): RetVal{ + protected listDevices(): RetVal { const deviceInfo: DeviceInfo = { - id: 'uniqieID', - name: 'My device', - icon: 'node', // find possible icons here: https://github.com/ioBroker/adapter-react-v5/blob/main/src/Components/DeviceType/DeviceTypeIcon.tsx#L68 - manufacturer: { objectId: 'uniqieID', property: 'native.manufacturer' }, - model: { objectId: 'uniqieID', property: 'native.model' }, - status: { - battery: { stateId: 'uniqieID.DevicePower0.BatteryPercent' }, - connection: { stateId: 'uniqieID.online', mapping: {'true': 'connected', 'false': 'disconnected'} }, - rssi: { stateId: 'uniqieID.rssi' }, - }, - hasDetails: true, + id: 'uniqieID', + name: 'My device', + icon: 'node', // find possible icons here: https://github.com/ioBroker/adapter-react-v5/blob/main/src/Components/DeviceType/DeviceTypeIcon.tsx#L68 + manufacturer: { objectId: 'uniqieID', property: 'native.manufacturer' }, + model: { objectId: 'uniqieID', property: 'native.model' }, + status: { + battery: { stateId: 'uniqieID.DevicePower0.BatteryPercent' }, + connection: { stateId: 'uniqieID.online', mapping: { true: 'connected', false: 'disconnected' } }, + rssi: { stateId: 'uniqieID.rssi' }, + }, + hasDetails: true, }; return [deviceInfo]; } } ``` - + + ## Changelog + ### 2.0.2 (2026-01-28) -* (@GermanBluefox) BREAKING: Admin/GUI must have version 9 (or higher) of `dm-gui-components` -* (@GermanBluefox) Added types to update status of device directly from state -* (@GermanBluefox) Added backend to GUI communication possibility -* (@GermanBluefox) Added `dm:deviceInfo` command -* (@GermanBluefox) Added `dm:deviceStatus` command + +- (@GermanBluefox) BREAKING: Admin/GUI must have version 9 (or higher) of `dm-gui-components` +- (@GermanBluefox) Added types to update status of device directly from state +- (@GermanBluefox) Added backend to GUI communication possibility +- (@GermanBluefox) Added `dm:deviceInfo` command +- (@GermanBluefox) Added `dm:deviceStatus` command ### 1.0.16 (2026-01-02) -* (@GermanBluefox) Added `ignoreApplyDisabled` flag -* (@GermanBluefox) Added `update` icon + +- (@GermanBluefox) Added `ignoreApplyDisabled` flag +- (@GermanBluefox) Added `update` icon ### 1.0.13 (2025-10-21) -* (@GermanBluefox) Updated packages + +- (@GermanBluefox) Updated packages ### 1.0.10 (2025-05-05) -* (@GermanBluefox) Added timeout property to actions -* (@GermanBluefox) Updated packages +- (@GermanBluefox) Added timeout property to actions +- (@GermanBluefox) Updated packages ### 1.0.9 (2025-01-25) -* (@GermanBluefox) Added copyToClipboard dialog button +- (@GermanBluefox) Added copyToClipboard dialog button ### 1.0.8 (2025-01-24) -* (@GermanBluefox) Removed `headerTextColor` to device info +- (@GermanBluefox) Removed `headerTextColor` to device info ### 1.0.6 (2025-01-14) - -* (@GermanBluefox) Added the connection type indication + +- (@GermanBluefox) Added the connection type indication ### 1.0.5 (2025-01-11) -* (@GermanBluefox) Added action ENABLE_DISABLE and `enabled` status +- (@GermanBluefox) Added action ENABLE_DISABLE and `enabled` status ### 1.0.0 (2025-01-08) -* (@GermanBluefox) Added `disabled` options for a device -* (@GermanBluefox) Major release just because is good enough. No breaking changes. +- (@GermanBluefox) Added `disabled` options for a device +- (@GermanBluefox) Major release just because is good enough. No breaking changes. ### 0.6.11 (2024-12-11) -* (@GermanBluefox) Do not close handler for progress +- (@GermanBluefox) Do not close handler for progress ### 0.6.10 (2024-12-10) -* (@GermanBluefox) Export `BackEndCommandJsonFormOptions` type +- (@GermanBluefox) Export `BackEndCommandJsonFormOptions` type ### 0.6.9 (2024-11-22) -* (@GermanBluefox) Added a max-width option for form +- (@GermanBluefox) Added a max-width option for form ### 0.6.8 (2024-11-22) -* (@GermanBluefox) Allowed grouping of devices +- (@GermanBluefox) Allowed grouping of devices ### 0.6.7 (2024-11-20) -* (@GermanBluefox) Updated types +- (@GermanBluefox) Updated types ### 0.6.6 (2024-11-18) -* (@GermanBluefox) Added configurable buttons for form +- (@GermanBluefox) Added configurable buttons for form ### 0.6.0 (2024-11-17) -* (@GermanBluefox) used new ioBroker/eslint-config lib and changed prettifier settings -* (@GermanBluefox) updated JsonConfig types +- (@GermanBluefox) used new ioBroker/eslint-config lib and changed prettifier settings +- (@GermanBluefox) updated JsonConfig types ### 0.5.0 (2024-08-30) -* (bluefox) Migrated to eslint 9 + +- (bluefox) Migrated to eslint 9 ### 0.4.0 (2024-08-30) -* (bluefox) Added `state` type for JSON config + +- (bluefox) Added `state` type for JSON config ### 0.3.1 (2024-07-18) -* (bluefox) Added qrCode type for JSON config + +- (bluefox) Added qrCode type for JSON config ### 0.3.0 (2024-07-17) -* (bluefox) packages updated -* (bluefox) Updated JSON config types + +- (bluefox) packages updated +- (bluefox) Updated JSON config types ### 0.2.2 (2024-06-26) -* (bluefox) packages updated + +- (bluefox) packages updated ### 0.2.0 (2024-05-29) -* (bluefox) enhanced type exports -* (bluefox) added confirmation and input text options + +- (bluefox) enhanced type exports +- (bluefox) added confirmation and input text options ### 0.1.9 (2023-12-25) -* (foxriver76) enhanced type exports + +- (foxriver76) enhanced type exports ### 0.1.8 (2023-12-17) -* (bluefox) corrected control error + +- (bluefox) corrected control error ### 0.1.7 (2023-12-17) -* (bluefox) added channel info + +- (bluefox) added channel info ### 0.1.5 (2023-12-16) -* (bluefox) extended controls with unit and new control types + +- (bluefox) extended controls with unit and new control types ### 0.1.4 (2023-12-13) -* (bluefox) added error codes + +- (bluefox) added error codes ### 0.1.3 (2023-12-10) -* (bluefox) added some fields to DeviceInfo interface -* (bluefox) added control possibilities + +- (bluefox) added some fields to DeviceInfo interface +- (bluefox) added control possibilities ## License + MIT License Copyright (c) 2023-2026 ioBroker Community Developers diff --git a/src/DeviceManagement.ts b/src/DeviceManagement.ts index adec7c6..60a42f5 100644 --- a/src/DeviceManagement.ts +++ b/src/DeviceManagement.ts @@ -2,18 +2,18 @@ import type { AdapterInstance } from '@iobroker/adapter-core'; import type { ActionContext } from './ActionContext'; import type { ProgressDialog } from './ProgressDialog'; import { + ErrorCodes, type ActionBase, + type ActionButton, type DeviceDetails, type DeviceInfo, + type DeviceStatus, type ErrorResponse, type InstanceDetails, type JsonFormData, type JsonFormSchema, type RefreshResponse, type RetVal, - type ActionButton, - ErrorCodes, - type DeviceStatus, } from './types'; import type * as api from './types/api'; import type { BackendToGuiCommand, ControlState, DeviceControl } from './types/base'; @@ -81,11 +81,11 @@ export abstract class DeviceManagement; - protected getDeviceInfo(deviceId: string): RetVal { + protected getDeviceInfo(_deviceId: string): RetVal { throw new Error('Do not send "infoUpdate" or "delete" command without implementing getDeviceInfo method!'); } - protected getDeviceStatus(deviceId: string): RetVal { + protected getDeviceStatus(_deviceId: string): RetVal { throw new Error('Do not send "statusUpdate" command without implementing getDeviceStatus method!'); } diff --git a/src/types/index.ts b/src/types/index.ts index 401a58a..052823c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,4 @@ export type * from './adapter'; +export { ACTIONS, type ChannelInfo, type Color, type ControlBase, type ControlState } from './base'; export type * from './common'; export * from './errorCodes'; -export { type ChannelInfo, type Color, type ControlState, type ControlBase, ACTIONS } from './base'; From f38104b7db4d5f125641d488c252cb531f306cfb Mon Sep 17 00:00:00 2001 From: Samuel Weibel Date: Mon, 16 Feb 2026 08:33:48 +0100 Subject: [PATCH 05/23] Update dynamic generated code from json-config --- src/types/common.ts | 180 ++++++++++++++++++++++++++++++-------------- tasks.js | 6 +- 2 files changed, 127 insertions(+), 59 deletions(-) diff --git a/src/types/common.ts b/src/types/common.ts index fdce485..14e06b1 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -50,7 +50,7 @@ interface ObjectBrowserCustomFilter { export type ObjectBrowserType = 'state' | 'instance' | 'channel' | 'device' | 'chart'; -// -- START OF DYNAMIC GENERATED CODE FROM https://raw.githubusercontent.com/ioBroker/ioBroker.admin/master/packages/jsonConfig/src/types.d.ts +// -- START OF DYNAMIC GENERATED CODE FROM https://raw.githubusercontent.com/ioBroker/json-config/main/src/types.d.ts export type ConfigItemType = | 'accordion' | 'alive' @@ -74,6 +74,8 @@ export type ConfigItemType = | 'fileSelector' | 'func' | 'header' + | 'iframe' + | 'iframeSendTo' | 'image' | 'imageSendTo' | 'infoBox' @@ -109,7 +111,8 @@ export type ConfigItemType = | 'timePicker' | 'topic' | 'user' - | 'uuid'; + | 'uuid' + | 'yamlEditor'; export type ConfigIconType = // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents @@ -165,8 +168,6 @@ export type ConfigIconType = // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'unpair' // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents - | 'update' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'upload' // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'user' @@ -224,7 +225,7 @@ export interface ConfigItem { default?: boolean | number | string; defaultFunc?: string; defaultSendTo?: string; - /** Allow saving of configuration even with error */ + /** Allow saving of configuration even with an error */ allowSaveWithError?: boolean; data?: string | number | boolean; jsonData?: string; @@ -260,21 +261,21 @@ interface ConfigItemIndexed extends ConfigItem { interface ConfigItemTableIndexed extends ConfigItem { attr?: string; - /** show filter options in the header of table */ + /** show filter options in the header of the table */ filter?: boolean; - /** show sorting options in the header of table */ + /** show sorting options in the header of the table */ sort?: boolean; - /** tooltip in the header of table */ + /** tooltip in the header of the table */ title?: string; } export interface ConfigItemAlive extends ConfigItem { type: 'alive'; - /** check if the instance is alive. If not defined, it will be used current instance. You can use `${data.number}` pattern in the text. */ + /** check if the instance is alive. If not defined, it will be used current instance. You can use the ` $ {data.number} ` pattern in the text. */ instance?: string; - /** default text is `Instance %s is alive`, where %s will be replaced by `ADAPTER.0`. The translation must exist in i18n files. */ + /** the default text is `Instance %s is alive`, where %s will be replaced by `ADAPTER.0`. The translation must exist in i18n files. */ textAlive?: string; - /** default text is `Instance %s is not alive`, where %s will be replaced by `ADAPTER.0`. The translation must exist in i18n files. */ + /** the default text is `Instance %s is not alive`, where %s will be replaced by `ADAPTER.0`. The translation must exist in i18n files. */ textNotAlive?: string; } @@ -337,17 +338,17 @@ export interface ConfigItemTabs extends ConfigItem { export interface ConfigItemText extends ConfigItem { type: 'text'; - /** max length of the text in field */ + /** max length of the text in the field */ maxLength?: number; /** @deprecated use maxLength */ max?: number; /** read-only field */ readOnly?: boolean; - /** show copy to clipboard button, but only if disabled or read-only */ + /** show copy-to-clipboard button, but only if disabled or read-only */ copyToClipboard?: boolean; /** default is true. Set this attribute to `false` if trim is not desired. */ trim?: boolean; - /** default is 1. Set this attribute to `2` or more if you want to have a textarea with more than one row. */ + /** the default is 1. Set this attribute to `2` or more if you want to have a textarea with more than one row. */ minRows?: number; /** max rows of textarea. Used only if `minRows` > 1. */ maxRows?: number; @@ -414,7 +415,7 @@ export interface ConfigItemPassword extends ConfigItem { visible?: boolean; /** The read-only flag. Visible is automatically true if readOnly is true */ readOnly?: boolean; - /** max length of the text in field */ + /** max length of the text in the field */ maxLength?: number; /** @deprecated use maxLength */ max?: number; @@ -423,6 +424,7 @@ export interface ConfigItemPassword extends ConfigItem { export interface ConfigItemObjectId extends ConfigItem { type: 'objectId'; /** Desired type: `channel`, `device`, ... (has only `state` by default). It is plural, because `type` is already occupied. */ + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents types?: ObjectBrowserType | ObjectBrowserType[]; /** Show only this root object and its children */ root?: string; @@ -465,7 +467,7 @@ export interface ConfigItemSlider extends ConfigItem { export interface ConfigItemTopic extends ConfigItem { type: 'topic'; - /** max length of the text in field */ + /** max length of the text in the field */ maxLength?: number; /** @deprecated use maxLength */ max?: number; @@ -515,11 +517,11 @@ export interface ConfigItemStaticText extends Omit { label?: ioBroker.StringOrTranslated; /** link. Link could be dynamic like `#tab-objects/customs/${data.parentId} */ href?: string; - /** target of the link: _self, _blank or window name. For relative links default is _self and for absolute - _blank */ + /** target of the link: _self, _blank or window name. For relative links the default is _self and for absolute - _blank */ target?: string; /** If the GUI should be closed after a link was opened (only if the target is equal to '_self') */ close?: boolean; - /** show a link as button */ + /** show a link as a button */ button?: boolean; /** type of button (`outlined`, `contained`, `text`) */ variant?: 'contained' | 'outlined' | 'text'; @@ -541,13 +543,13 @@ export interface ConfigItemStaticInfo extends Omit { unit?: ioBroker.StringOrTranslated; /** Normally, the title and value are shown on the left and right of the line. With this flag, the value will appear just after the label*/ narrow?: boolean; - /** Add to label the colon at the end if not exist in label */ + /** Add to the label the colon at the end if not exist in the label */ addColon?: boolean; /** Value should blink when updated (true or color) */ blinkOnUpdate?: boolean | string; /** Value should blink continuously (true or color) */ blink?: boolean | string; - /** Show copy to clipboard button for value */ + /** Show a copy-to-clipboard button for value */ copyToClipboard?: boolean; /** Label style */ styleLabel?: CustomCSSProperties; @@ -559,7 +561,7 @@ export interface ConfigItemStaticInfo extends Omit { size?: number | 'small' | 'normal' | 'large'; /** Highlight line on mouse over */ highlight?: boolean; - /** Show boolean values as checkbox */ + /** Show boolean values as a checkbox */ booleanAsCheckbox?: boolean; /** Show string values as HTML */ html?: boolean; @@ -612,7 +614,7 @@ export interface ConfigItemSelect extends ConfigItem { attr?: string; /** If multiple selection is possible. In this case, the value will be an array */ multiple?: boolean; - /** show item even if no label was found for it (by multiple), default=`true` */ + /** show an item even if no label was found for it (by multiple), default=`true` */ showAllValues?: boolean; } @@ -630,7 +632,7 @@ export interface ConfigItemSetState extends ConfigItem { ack?: boolean; /** '${data.myText}_test' or number. Type will be detected automatically from the state type and converting done too */ val: ioBroker.StateValue; - /** Alert which will be shown by pressing the button */ + /** Alert, which will be shown by pressing the button */ okText?: ioBroker.StringOrTranslated; variant?: 'contained' | 'outlined'; color?: 'primary' | 'secondary' | 'grey'; @@ -645,7 +647,7 @@ export interface ConfigItemAutocompleteSendTo extends Omit { options?: (string | ConfigItemSelectOption)[]; data?: Record; freeSolo?: boolean; - /** max length of the text in field */ + /** max length of the text in the field */ maxLength?: number; /** @deprecated use maxLength */ max?: string; @@ -658,7 +660,7 @@ export interface ConfigItemAccordion extends ConfigItem { titleAttr?: string; /** If delete or add disabled, If noDelete is false, add, delete and move up/down should work */ noDelete?: boolean; - /** If clone button should be shown. If true, the clone button will be shown. If attribute name, this name will be unique. */ + /** If the clone button should be shown. If true, the clone button will be shown. If attribute name, this name will be unique. */ clone?: boolean | string; /** Items of accordion */ items: ConfigItemIndexed[]; @@ -689,7 +691,7 @@ export interface ConfigItemCoordinates extends ConfigItem { latitudeName?: string; /** if defined, the checkbox with "Use system settings" will be shown and latitude, longitude will be read from system.config, a boolean will be saved to the given name */ useSystemName?: string; - /** max length of the text in field */ + /** max length of the text in the field */ maxLength?: number; /** @deprecated use maxLength */ max?: number; @@ -711,7 +713,7 @@ export interface ConfigItemCustom extends ConfigItem { export interface ConfigItemDatePicker extends ConfigItem { type: 'datePicker'; - /** max length of the text in field */ + /** max length of the text in the field */ maxLength?: number; /** @deprecated use maxLength */ max?: number; @@ -734,6 +736,28 @@ export interface ConfigItemPort extends ConfigItem { readOnly?: boolean; } +export interface ConfigItemIFrame extends ConfigItem { + type: 'iframe'; + /* If URL is defined, the data attribute will be ignored. For static URLs */ + url?: string; + sandbox?: string; + allowFullscreen?: boolean; + frameBorder?: number | string; + /** if true, the iframe will be loaded only when it becomes visible */ + lazyLoad?: 'lazy' | 'eager'; + /** if true, the iframe will be reloaded every time it becomes visible */ + reloadOnShow?: boolean; + /** CSS Styles in React format (`marginLeft` and not `margin-left`) for the IFrame component */ + innerStyle?: CustomCSSProperties; +} + +export interface ConfigItemIFrameSendTo extends Omit { + type: 'iframeSendTo'; + command?: string; + alsoDependsOn?: string[]; + data?: Record; +} + export interface ConfigItemImageSendTo extends Omit { type: 'imageSendTo'; command?: string; @@ -747,8 +771,10 @@ export interface ConfigItemSendTo extends Omit { command?: string; jsonData?: string; data?: Record; - result?: string; - error?: string; + /** Translations for possible result codes. E.g. `{"RESULT_OK": "Operation successful"}`. The translation must exist in i18n files. */ + result?: Record; + /** Translations for possible error codes. E.g. `{"ERR_NO_OBJECT": "Object not found"}`. The translation must exist in i18n files. */ + error?: Record; variant?: 'contained' | 'outlined'; openUrl?: boolean; reloadBrowser?: boolean; @@ -764,13 +790,13 @@ export interface ConfigItemSendTo extends Omit { alsoDependsOn?: string[]; container?: 'text' | 'div' | 'html'; copyToClipboard?: boolean; - /** Styles for button itself */ + /** Styles for the button itself */ controlStyle?: CustomCSSProperties; } export interface ConfigItemState extends ConfigItem { type: 'state'; - /** Describes, which object ID should be taken for the controlling. The ID is without `ADAPTER.I.` prefix */ + /** Describes which object ID should be taken for the controlling. The ID is without `ADAPTER.I.` prefix */ oid: string; /** The `oid` is absolute and no need to add `ADAPTER.I` or `system.adapter.ADAPTER.I.` to oid */ foreign?: boolean; @@ -780,7 +806,7 @@ export interface ConfigItemState extends ConfigItem { control?: 'text' | 'html' | 'input' | 'slider' | 'select' | 'button' | 'switch' | 'number'; /** If true, the state will be shown as switch, select, button, slider or text input. Used only if no control property is defined */ controlled?: boolean; - /** Add unit to the value */ + /** Add a unit to the value */ unit?: string; /** this text will be shown if the value is true */ trueText?: string; @@ -810,15 +836,15 @@ export interface ConfigItemState extends ConfigItem { labelIcon?: string; /** Normally, the title and value are shown on the left and right of the line. With this flag, the value will appear just after the label*/ narrow?: boolean; - /** Add to label the colon at the end if not exist in label */ + /** Add to the label the colon at the end if not exist in the label */ addColon?: boolean; /** Value should blink when updated (true or color) */ blinkOnUpdate?: boolean | string; /** Font size */ size?: number | 'small' | 'normal' | 'large'; - /** Optional value, that will be sent for button */ + /** Optional value that will be sent for the button */ buttonValue?: ioBroker.StateValue; - /** Show SET button. The value in this case will be sent only when the button is pressed. You can define the text of the button. Default text is "Set" */ + /** Show the SET button. The value in this case will be sent only when the button is pressed. You can define the text of the button. The default text is "Set" */ showEnterButton?: boolean | ioBroker.StringOrTranslated; /** The value in this case will be sent only when the "Enter" button is pressed. It can be combined with `showEnterButton` */ setOnEnterKey?: boolean; @@ -829,7 +855,7 @@ export interface ConfigItemState extends ConfigItem { export interface ConfigItemTextSendTo extends Omit { type: 'textSendTo'; container?: 'text' | 'div'; - /** if true - show copy to clipboard button */ + /** if true - show copy-to-clipboard button */ copyToClipboard?: boolean; /** by change of which attributes, the command must be resent */ alsoDependsOn?: string[]; @@ -843,11 +869,11 @@ export interface ConfigItemTextSendTo extends Omit { export interface ConfigItemSelectSendTo extends Omit { type: 'selectSendTo'; - /** allow manual editing. Without drop-down menu (if instance is offline). Default `true`. */ + /** allow manual editing. Without a drop-down menu (if the instance is offline). Default `true`. */ manual?: boolean; /** Multiple choice select */ multiple?: boolean; - /** show item even if no label was found for it (by multiple), default=`true` */ + /** show an item even if no label was found for it (by multiple), default=`true` */ showAllValues?: boolean; /** if true, the clear button will not be shown */ noClearButton?: boolean; @@ -864,21 +890,23 @@ export interface ConfigItemSelectSendTo extends Omit { export interface ConfigItemTable extends ConfigItem { type: 'table'; items?: ConfigItemTableIndexed[]; + /** Define the name of the attribute of the item which should be shown as a title of the item in cards mode. */ + titleAttribute?: string; /** If delete or add disabled, If noDelete is false, add, delete and move up/down should work */ noDelete?: boolean; /** @deprecated don't use */ objKeyName?: string; /** @deprecated don't use */ objValueName?: string; - /** If add allowed even if filter is set */ + /** If add allowed even if a filter is set */ allowAddByFilter?: boolean; - /** The number of lines from which the second add button at the bottom of the table will be shown. Default 5 */ + /** The number of lines from which the second adding button at the bottom of the table will be shown. Default 5 */ showSecondAddAt?: number; - /** Show first plus button on top of the first column and not on the left. */ + /** Show the first plus button on top of the first column and not on the left. */ showFirstAddOnTop?: boolean; - /** If clone button should be shown. If true, the clone button will be shown. If attribute name, this name will be unique. */ + /** If the clone button should be shown. If true, the clone button will be shown. If attribute name, this name will be unique. */ clone?: boolean | string; - /** If export button should be shown. Export as csv file. */ + /** If export button should be shown. Export as a csv file. */ export?: boolean; /** If import button should be shown. Import from csv file. */ import?: boolean; @@ -886,7 +914,7 @@ export interface ConfigItemTable extends ConfigItem { compact?: boolean; /** Specify the 'attr' name of columns which need to be unique */ uniqueColumns?: string[]; - /** These items will be encrypted before saving with simple (not SHA) encryption method */ + /** These items will be encrypted before saving with a simple (not SHA) encryption method */ encryptedAttributes?: string[]; /** Breakpoint that will be rendered as cards */ useCardFor?: ('xs' | 'sm' | 'md' | 'lg' | 'xl')[]; @@ -972,7 +1000,19 @@ export interface ConfigItemJsonEditor extends ConfigItem { allowEmpty?: boolean; /** Allow JSON5 format. Default is disabled */ json5?: boolean; - /** Do not allow to save the value if error in JSON or JSON5 */ + /** Do not allow saving the value if error in JSON or JSON5 */ + doNotApplyWithError?: boolean; + /** Open the editor in read-only mode - editor can be opened but content cannot be modified */ + readOnly?: boolean; +} + +export interface ConfigItemYamlEditor extends ConfigItem { + type: 'yamlEditor'; + /** if false, the text will be not validated as YAML */ + validateYaml?: boolean; + /** if true, the YAML will be validated only if the value is not empty */ + allowEmpty?: boolean; + /** Do not allow saving the value if error in YAML */ doNotApplyWithError?: boolean; /** Open the editor in read-only mode - editor can be opened but content cannot be modified */ readOnly?: boolean; @@ -988,7 +1028,7 @@ export interface ConfigItemInterface extends ConfigItem { export interface ConfigItemImageUpload extends ConfigItem { type: 'image'; - /** name of a file is structure name. In the below example `login-bg.png` is file name for `writeFile("myAdapter.INSTANCE", "login-bg.png")` */ + /** the name of a file is the structure name. In the below example `login-bg.png` is the file name for `writeFile("myAdapter.INSTANCE", "login-bg.png")` */ filename?: string; /** HTML accept attribute, like `{ 'image/**': [], 'application/pdf': ['.pdf'] }`, default `{ 'image/*': [] }` */ accept?: Record; @@ -996,17 +1036,17 @@ export interface ConfigItemImageUpload extends ConfigItem { maxSize?: number; /** if true, the image will be saved as data-url in attribute, elsewise as binary in file storage */ base64?: boolean; - /** if true, allow user to crop the image */ + /** if true, allow the user to crop the image */ crop?: boolean; } export interface ConfigItemInstanceSelect extends ConfigItem { type: 'instance'; - /** name of adapter. With special name `_dataSources` you can get all adapters with flag `common.getHistory`. */ + /** name of adapter. With the special name `_dataSources` you can get all adapters with the flag `common.getHistory`. */ adapter?: string; - /** optional list of adapters, that should be shown. If not defined, all adapters will be shown. Only active if `adapter` attribute is not defined. */ + /** optional list of adapters that should be shown. If not defined, all adapters will be shown. Only active if the ` adapter ` attribute is not defined. */ adapters?: string[]; - /** if true. Additional option "deactivate" is shown */ + /** if true. The additional option "deactivate" is shown */ allowDeactivate?: boolean; /** if true. Only enabled instances will be shown */ onlyEnabled?: boolean; @@ -1020,7 +1060,7 @@ export interface ConfigItemInstanceSelect extends ConfigItem { export interface ConfigItemFile extends ConfigItem { type: 'file'; - /** if a user can manually enter the file name and not only through select dialog */ + /** if a user can manually enter the file name and not only through a select dialog */ disableEdit?: boolean; /** limit selection to one specific object of type `meta` and the following path (not mandatory) */ limitPath?: string; @@ -1036,7 +1076,7 @@ export interface ConfigItemFile extends ConfigItem { allowView?: boolean; /** show toolbar (default true) */ showToolbar?: boolean; - /** user can select only folders (e.g., for uploading path) */ + /** user can select only folders (e.g., for the uploading path) */ selectOnlyFolders?: boolean; /** trim the filename */ trim?: boolean; @@ -1048,15 +1088,15 @@ export interface ConfigItemFile extends ConfigItem { export interface ConfigItemFileSelector extends ConfigItem { type: 'fileSelector'; - /** File extension pattern. Allowed `**\/*.ext` to show all files from subfolders too, `*.ext` to show from root folder or `folderName\/*.ext` to show all files in sub-folder `folderName`. Default `**\/*.*`. */ + /** File extension pattern. Allowed `**\/*.ext` to show all files from subfolders too, `*.ext` to show from the root folder or `folderName\/*.ext` to show all files in subfolder `folderName`. Default `**\/*.*`. */ pattern: string; /** type of files: `audio`, `image`, `text` */ fileTypes?: 'audio' | 'image' | 'text'; - /** Object ID of type `meta`. You can use special placeholder `%INSTANCE%`: like `myAdapter.%INSTANCE%.files` */ + /** Object ID of the type `meta`. You can use special placeholder `%INSTANCE%`: like `myAdapter.%INSTANCE%.files` */ objectID?: string; /** path, where the uploaded files will be stored. Like `folderName`. If not defined, no upload field will be shown. To upload in the root, set this field to `/`. */ upload?: string; - /** Show refresh button near the select. */ + /** Show the refresh button near the select. */ refresh?: boolean; /** max file size (default 2MB) */ maxSize?: number; @@ -1114,11 +1154,14 @@ export type ConfigItemAny = | ConfigItemCRON | ConfigItemFile | ConfigItemFileSelector + | ConfigItemIFrame + | ConfigItemIFrameSendTo | ConfigItemImageSendTo | ConfigItemInstanceSelect | ConfigItemImageUpload | ConfigItemInterface | ConfigItemJsonEditor + | ConfigItemYamlEditor | ConfigItemLicense | ConfigItemPassword | ConfigItemSetState @@ -1131,6 +1174,33 @@ export type ConfigItemAny = | ConfigItemObjectId | ConfigItemQrCode; +export type JsonConfigContext = { + adapterName: string; + dateFormat: string; + forceUpdate: (attr: string | string[], data: any) => void; + instance: number; + isFloatComma: boolean; + socket: AdminConnection; + systemConfig: ioBroker.SystemConfigCommon; + theme: IobTheme; + themeType: ThemeType; + _themeName: ThemeName; + + DeviceManager?: React.FC; + changeLanguage?: () => void; + customs?: Record; + embedded?: boolean; + imagePrefix?: string; + instanceObj?: ioBroker.InstanceObject; + /** If true, this field edits multiple data points at once and thus contains an array, should not be saved if not changed */ + multiEdit?: boolean; + /** Backend request to refresh data */ + onBackEndCommand?: (command?: BackEndCommand) => void; + onCommandRunning: (commandRunning: boolean) => void; + onValueChange?: (attr: string, value: any, saveConfig: boolean) => void; + registerOnForceUpdate?: (attr: string, cb?: (data: any) => void) => void; +}; + // Notification GUI export type BackEndCommandType = 'nop' | 'refresh' | 'link' | 'message'; diff --git a/tasks.js b/tasks.js index c9ddca0..1a5f764 100644 --- a/tasks.js +++ b/tasks.js @@ -1,13 +1,11 @@ const { readFileSync, writeFileSync } = require('node:fs'); const axios = require('axios'); const COMMON_FILENAME = `${__dirname}/src/types/common.ts`; -const TYPES_URL = 'https://raw.githubusercontent.com/ioBroker/ioBroker.admin/master/packages/jsonConfig/src/types.d.ts'; +const TYPES_URL = 'https://raw.githubusercontent.com/ioBroker/json-config/main/src/types.d.ts'; async function patchCommonTs() { let text = readFileSync(COMMON_FILENAME).toString(); - const response = await axios.get( - 'https://raw.githubusercontent.com/ioBroker/ioBroker.admin/master/packages/jsonConfig/src/types.d.ts', - ); + const response = await axios.get(TYPES_URL); const typeLines = response.data.toString().split('\n'); const lines = text.split('\n'); From 39b865e1253ea4e7cdb5f7386683a47779f9aa4c Mon Sep 17 00:00:00 2001 From: Samuel Weibel Date: Mon, 16 Feb 2026 08:38:24 +0100 Subject: [PATCH 06/23] Fix issue with JsonConfigContext referencing React --- src/types/common.ts | 73 --------------------------------------------- tasks.js | 9 ++++++ 2 files changed, 9 insertions(+), 73 deletions(-) diff --git a/src/types/common.ts b/src/types/common.ts index 14e06b1..b59c23d 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -1174,79 +1174,6 @@ export type ConfigItemAny = | ConfigItemObjectId | ConfigItemQrCode; -export type JsonConfigContext = { - adapterName: string; - dateFormat: string; - forceUpdate: (attr: string | string[], data: any) => void; - instance: number; - isFloatComma: boolean; - socket: AdminConnection; - systemConfig: ioBroker.SystemConfigCommon; - theme: IobTheme; - themeType: ThemeType; - _themeName: ThemeName; - - DeviceManager?: React.FC; - changeLanguage?: () => void; - customs?: Record; - embedded?: boolean; - imagePrefix?: string; - instanceObj?: ioBroker.InstanceObject; - /** If true, this field edits multiple data points at once and thus contains an array, should not be saved if not changed */ - multiEdit?: boolean; - /** Backend request to refresh data */ - onBackEndCommand?: (command?: BackEndCommand) => void; - onCommandRunning: (commandRunning: boolean) => void; - onValueChange?: (attr: string, value: any, saveConfig: boolean) => void; - registerOnForceUpdate?: (attr: string, cb?: (data: any) => void) => void; -}; - -// Notification GUI - -export type BackEndCommandType = 'nop' | 'refresh' | 'link' | 'message'; - -export interface BackEndCommandGeneric { - command: BackEndCommandType; - /** New GUI schema */ - schema?: ConfigItemPanel | ConfigItemTabs; - /** New GUI data */ - data?: Record; - refresh?: boolean; -} - -export interface BackEndCommandNoOperation extends BackEndCommandGeneric { - command: 'nop'; -} - -export interface BackEndCommandRefresh extends BackEndCommandGeneric { - command: 'refresh'; - /** If refresh the GUI */ - fullRefresh?: boolean; -} - -export interface BackEndCommandOpenLink extends BackEndCommandGeneric { - command: 'link'; - /** Link url. Could be relative ('#blabla') or absolute ('https://blabla') */ - url: string; - /** Target of the link. Default is `_self` for relative and '_blank' for absolute links */ - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents - target?: '_self' | '_blank' | string; - /** If GUI should be closed after the link was opened (Only for target='_self') */ - close?: boolean; -} - -export interface BackEndCommandMessage extends BackEndCommandGeneric { - command: 'message'; - /** Message text */ - message: ioBroker.StringOrTranslated; - /** If GUI should be closed after the message shown */ - close?: boolean; - /** Type of message. Default is 'popup' */ - variant: 'popup' | 'dialog'; -} - -export type BackEndCommand = BackEndCommandMessage | BackEndCommandOpenLink | BackEndCommandRefresh; - // -- STOP OF DYNAMIC GENERATED CODE export type ActionButton = { diff --git a/tasks.js b/tasks.js index 1a5f764..241b16e 100644 --- a/tasks.js +++ b/tasks.js @@ -41,6 +41,15 @@ async function patchCommonTs() { } typeLines.splice(0, f); + while (!typeLines[f].startsWith('export type JsonConfigContext =')) { + f++; + } + if (f >= typeLines.length) { + console.error(`Cannot find "export type JsonConfigContext =" in ${TYPES_URL})`); + process.exit(2); + } + typeLines.splice(f); + writeFileSync(COMMON_FILENAME, `${start.join('\n')}\n${typeLines.join('\n')}\n${end.join('\n')}`); } From ab9b4b1a190ab74d65680fca9759028e2dc2e51c Mon Sep 17 00:00:00 2001 From: Samuel Weibel Date: Mon, 16 Feb 2026 10:57:27 +0100 Subject: [PATCH 07/23] Allow device list to load incrementally Closes #14 --- README.md | 8 +- examples/dm-test.ts | 16 ++-- src/DeviceManagement.ts | 194 ++++++++++++++++++++++++++++------------ src/types/api.ts | 6 ++ 4 files changed, 155 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index add75ca..fda66c2 100644 --- a/README.md +++ b/README.md @@ -113,17 +113,17 @@ All methods can either return an object of the defined value or a `Promise` reso This allows you to implement the method synchronously or asynchronously, depending on your implementation. -### `listDevices()` +### `loadDevices(context: DeviceLoadContext)` This method must always be overridden (as it is abstract in the base class). -You must return an array with information about all devices of this adapter's instance. +You must fill the `context` with information about all devices of this adapter's instance. This method is called when the user expands an instance in the list. -In most cases, you will get all states of your instance and fill the array with the relevant information. +In most cases, you will get all states of your instance and fill the `context` with the relevant information. -Every array entry is an object of type `DeviceInfo` which has the following properties: +Every item is an object of type `DeviceInfo` which has the following properties: - `id` (string): a unique (human-readable) identifier of the device (it must be unique for your adapter instance only) - `name` (string or translations): the human-readable name of this device diff --git a/examples/dm-test.ts b/examples/dm-test.ts index a08b16e..e53852c 100644 --- a/examples/dm-test.ts +++ b/examples/dm-test.ts @@ -80,11 +80,10 @@ const demoFormSchema: JsonFormSchema = { }; export class DmTestDeviceManagement extends DeviceManagement { - protected listDevices(): Promise[]> { - return Promise.resolve([ - { id: 'test-123', name: 'Test 123', status: 'connected' }, - { id: 'test-345', name: 'Test 345', status: 'disconnected', hasDetails: true, actions: [] }, - { + protected loadDevices(context: DeviceLoadContext): void { + context.addDevice({ id: 'test-123', name: 'Test 123', status: 'connected' }); + context.addDevice({ id: 'test-345', name: 'Test 345', status: 'disconnected', hasDetails: true, actions: [] }); + context.addDevice({ id: 'test-789', name: 'Test 789', status: 'connected', @@ -104,8 +103,8 @@ export class DmTestDeviceManagement extends DeviceManagement { description: 'Forward', }, ], - }, - { + }); + context.addDevice({ id: 'test-ABC', name: 'Test ABC', status: 'connected', @@ -116,8 +115,7 @@ export class DmTestDeviceManagement extends DeviceManagement { description: 'Show forms flow', }, ], - }, - ]); + }); } protected override async handleDeviceAction( diff --git a/src/DeviceManagement.ts b/src/DeviceManagement.ts index 60a42f5..3fc59c4 100644 --- a/src/DeviceManagement.ts +++ b/src/DeviceManagement.ts @@ -18,12 +18,18 @@ import { import type * as api from './types/api'; import type { BackendToGuiCommand, ControlState, DeviceControl } from './types/base'; +export type DeviceLoadContext = { + addDevice(device: DeviceInfo): void; + setTotalDevices(total: number): void; +}; + export abstract class DeviceManagement { private instanceInfo?: InstanceDetails; private devices?: Map; private readonly communicationStateId: string; - private readonly contexts = new Map(); + private readonly deviceLoadContexts = new Map(); + private readonly messageContexts = new Map(); constructor( protected readonly adapter: T, @@ -79,7 +85,7 @@ export abstract class DeviceManagement; + protected abstract loadDevices(context: DeviceLoadContext): RetVal; protected getDeviceInfo(_deviceId: string): RetVal { throw new Error('Do not send "infoUpdate" or "delete" command without implementing getDeviceInfo method!'); @@ -289,29 +295,26 @@ export abstract class DeviceManagement( - { ...this.instanceInfo, actions: this.convertActions(this.instanceInfo.actions) }, + { ...this.instanceInfo, actions: convertActions(this.instanceInfo.actions) }, msg, ); return; } - case 'dm:listDevices': { - const deviceList = await this.listDevices(); + case 'dm:loadDevices': { + const context = new DeviceLoadContextImpl(msg, this.adapter); + this.deviceLoadContexts.set(msg._id, context); + await this.loadDevices(context); + if (context.complete()) { + this.deviceLoadContexts.delete(msg._id); + } - this.devices = deviceList.reduce((map, value) => { + this.devices = context.devices.reduce((map, value) => { if (map.has(value.id)) { throw new Error(`Device ID ${value.id} is not unique`); } map.set(value.id, value); return map; }, new Map()); - - const apiDeviceList: api.DeviceInfo[] = deviceList.map(d => ({ - ...d, - actions: this.convertActions(d.actions), - controls: this.convertControls(d.controls), - })); - - this.sendReply(apiDeviceList, msg); return; } case 'dm:deviceInfo': { @@ -319,8 +322,8 @@ export abstract class DeviceManagement( { ...deviceInfo, - actions: this.convertActions(deviceInfo.actions), - controls: this.convertControls(deviceInfo.controls), + actions: convertActions(deviceInfo.actions), + controls: convertControls(deviceInfo.controls), }, msg, ); @@ -339,34 +342,34 @@ export abstract class DeviceManagement(actions?: T[]): undefined | U[] { - if (!actions) { - return undefined; - } + private sendReply(reply: T, msg: ioBroker.Message): void { + this.adapter.sendTo(msg.from, msg.command, reply, msg.callback); + } +} - // detect duplicate IDs - const ids = new Set(); +class DeviceLoadContextImpl implements DeviceLoadContext { + private readonly minBatchSize = 8; + public readonly devices: DeviceInfo[] = []; + private readonly id: number; + private sendNext: api.DeviceInfo[] = []; + private totalDevices?: number; + private completed = false; + private respondTo?: ioBroker.Message; - actions.forEach(a => { - if (ids.has(a.id)) { - throw new Error(`Action ID ${a.id} is used twice, this would lead to unexpected behavior`); - } - ids.add(a.id); + constructor( + msg: ioBroker.Message, + private readonly adapter: AdapterInstance, + ) { + this.respondTo = msg; + this.id = msg._id; + } + + addDevice(device: DeviceInfo): void { + this.devices.push(device); + this.sendNext.push({ + ...device, + actions: convertActions(device.actions), + controls: convertControls(device.controls), }); + this.flush(); + } - // remove handler function to send it as JSON - return actions.map((a: any) => ({ ...a, handler: undefined, disabled: !a.handler })); + setTotalDevices(total: number): void { + this.totalDevices = total; + this.flush(); } - private convertControls, U extends DeviceControl<'api'>>( - controls?: T[], - ): undefined | U[] { - if (!controls) { - return undefined; - } + complete(): boolean { + this.completed = true; + return this.flush(); + } - // detect duplicate IDs - const ids = new Set(); + handleProgress(message: ioBroker.Message): boolean { + this.respondTo = message; + return this.flush(); + } - controls.forEach(a => { - if (ids.has(a.id)) { - throw new Error(`Control ID ${a.id} is used twice, this would lead to unexpected behavior`); - } - ids.add(a.id); - }); + private flush(): boolean { + if (this.sendNext.length <= this.minBatchSize && !this.completed) { + return false; + } - // remove handler function to send it as JSON - return controls.map((a: any) => ({ ...a, handler: undefined, getStateHandler: undefined })); - } + if (!this.respondTo) { + return false; + } - private sendReply(reply: T, msg: ioBroker.Message): void { + const reply: api.DeviceLoadIncrement = { + add: this.sendNext, + total: this.totalDevices, + next: this.completed ? undefined : { origin: this.id }, + }; + this.sendNext = []; + + const msg = this.respondTo; + this.respondTo = undefined; this.adapter.sendTo(msg.from, msg.command, reply, msg.callback); + return this.completed; } } @@ -603,3 +645,43 @@ export class MessageContext implements ActionContext { } } } + +function convertActions(actions?: T[]): undefined | U[] { + if (!actions) { + return undefined; + } + + // detect duplicate IDs + const ids = new Set(); + + actions.forEach(a => { + if (ids.has(a.id)) { + throw new Error(`Action ID ${a.id} is used twice, this would lead to unexpected behavior`); + } + ids.add(a.id); + }); + + // remove handler function to send it as JSON + return actions.map((a: any) => ({ ...a, handler: undefined, disabled: !a.handler })); +} + +function convertControls, U extends DeviceControl<'api'>>( + controls?: T[], +): undefined | U[] { + if (!controls) { + return undefined; + } + + // detect duplicate IDs + const ids = new Set(); + + controls.forEach(a => { + if (ids.has(a.id)) { + throw new Error(`Control ID ${a.id} is used twice, this would lead to unexpected behavior`); + } + ids.add(a.id); + }); + + // remove handler function to send it as JSON + return controls.map((a: any) => ({ ...a, handler: undefined, getStateHandler: undefined })); +} diff --git a/src/types/api.ts b/src/types/api.ts index f613bd5..855f426 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -6,3 +6,9 @@ export type DeviceAction = base.DeviceAction<'api'>; export type InstanceDetails = base.InstanceDetails<'api'>; export type DeviceInfo = base.DeviceInfo<'api'>; export type DeviceControl = base.DeviceControl<'api'>; + +export type DeviceLoadIncrement = { + add: DeviceInfo[]; + total?: number; + next?: { origin: number }; +}; From 2afd43acc8ade4e7a2a3cac79a8554dc78e5e44d Mon Sep 17 00:00:00 2001 From: Samuel Weibel Date: Mon, 16 Feb 2026 10:58:43 +0100 Subject: [PATCH 08/23] Simplify icon type (union with string) --- src/types/base.ts | 33 +++------------------------------ 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/src/types/base.ts b/src/types/base.ts index 928a741..b6e90b9 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -28,63 +28,36 @@ export interface ActionBase { /** * This can either be base64 or the URL to an icon. */ - icon?: // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents - | 'edit' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents + icon?: + | 'edit' | 'rename' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'delete' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'refresh' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'newDevice' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'new' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'add' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'discover' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'search' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'unpairDevice' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'pairDevice' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'identify' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'play' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'stop' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'pause' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'forward' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'next' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'rewind' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'previous' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'lamp' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'light' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'backlight' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'dimmer' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'socket' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'settings' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'users' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'group' - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents | 'user' - | string; // base64 or url + | (string & {}); // base64 or url description?: ioBroker.StringOrTranslated; disabled?: T extends 'api' ? boolean : never; color?: Color; From da13e6051c4fd8c415c3c020b48ed42532958940 Mon Sep 17 00:00:00 2001 From: Samuel Weibel Date: Mon, 16 Feb 2026 11:14:25 +0100 Subject: [PATCH 09/23] Removed direct access to `DeviceManagement.handleXxx()` methods --- README.md | 47 +++++++++--------- examples/dm-test.ts | 105 ++++++++++++++++++---------------------- src/DeviceManagement.ts | 8 +-- 3 files changed, 76 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index fda66c2..a9ea3cd 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ Every item is an object of type `DeviceInfo` which has the following properties: - `id` (string): unique identifier to recognize an action (never shown to the user) - `icon` (string): an icon shown on the button (see below for details) - `description` (string, optional): a text that will be shown as a tooltip on the button - - `disabled` (boolean, optional): if set to `true`, the button can't be clicked but is shown to the user + - `handler` (function, optional): function that will be called when the user clicks on the button; if not given, the button will be disabled in the UI - `hasDetails` (boolean, optional): if set to `true`, the row of the device can be expanded and details are shown below Possible strings for device icons are here: [TYPE ICONS](https://github.com/ioBroker/adapter-react-v5/blob/main/src/Components/DeviceType/DeviceTypeIcon.tsx#L68) @@ -158,7 +158,7 @@ If you override this method, the returned object must contain: - `icon` (string): an icon shown on the button (see below for details) - `title` (string): the title shown next to the icon on the button - `description` (string, optional): a text that will be shown as a tooltip on the button - - `disabled` (boolean, optional): if set to `true`, the button can't be clicked but is shown to the user + - `handler` (function, optional): function that will be called when the user clicks on the button; if not given, the button will be disabled in the UI - `communicationStateId` (string) (optional): the ID of the state that is used by backend for communication with front-end (only API v2) ### `getDeviceDetails(id: string)` @@ -175,14 +175,14 @@ For more details about the schema, see [here](https://github.com/ioBroker/ioBrok Please keep in mind that there is no "Save" button, so in most cases, the form shouldn't contain editable fields, but you may use `sendTo` objects to send data to the adapter. -### `handleInstanceAction(actionId: string, context: ActionContext) +### InstanceInfo action handlers -This method is called when to user clicks on an action (i.e., button) for an adapter instance. +These methods are called when the user clicks on an action (i.e., button) for an adapter instance. -The parameters of this method are: +The parameters of this function are: -- `actionId` (string): the `id` that was given in `getInstanceInfo()` --> `actions[].id` - `context` (object): object containing helper methods that can be used when executing the action +- `options` (object): object containing the action `value` (if given) The returned object must contain: @@ -192,15 +192,15 @@ This method can be implemented asynchronously and can take a lot of time to comp See below for how to interact with the user. -### `handleDeviceAction(deviceId: string, actionId: string, context: ActionContext) +### DeviceInfo action handlers -This method is called when the user clicks on an action (i.e., button) for a device. +These methods are called when the user clicks on an action (i.e., button) for an adapter instance. -The parameters of this method are: +The parameters of this function are: -- `deviceId` (string): the `id` that was given in `listDevices()` --> `[].id` -- `actionId` (string): the `id` that was given in `listDevices()` --> `[].actions[].id` +- `deviceId` (string): the `id` of the device - `context` (object): object containing helper methods that can be used when executing the action +- `options` (object): object containing the action `value` (if given) The returned object must contain: @@ -213,26 +213,24 @@ This method can be implemented asynchronously and can take a lot of time to comp See below for how to interact with the user. -### `handleDeviceControl(deviceId: string, controlId: string, state: ControlState, context: MessageContext) +### DeviceInfo control handlers -This method is called when the user clicks on a control (i.e., slider) in the device card. +These functions are called when the user clicks on a control (i.e., slider) in the device card. The parameters of this method are: - `deviceId` (string): the `id` that was given in `listDevices()` --> `[].id` - `controlId` (string): the `id` that was given in `listDevices()` --> `[].controls[].id`. There are some reserved control names, you can find the list below. -- `state` (string | number | boolean): new state for the control, that will be sent to a real device +- `newState` (string | number | boolean): new state for the control, that will be sent to a real device - `context` (object): object containing helper methods that can be used when executing the action -The returned object must contain: - -- `state`: ioBroker state object +The returned object must be an ioBroker state object. This method can be implemented asynchronously and can take a lot of time to complete. -### `handleDeviceControlState(deviceId: string, controlId: string, context: MessageContext) +### DeviceInfo getState handlers -This method is called when GUI requests the update of the state. +These functions are called when GUI requests the update of the state. The parameters of this method are: @@ -240,9 +238,7 @@ The parameters of this method are: - `controlId` (string): the `id` that was given in `listDevices()` --> `[].controls[].id` - `context` (object): object containing helper methods that can be used when executing the action -The returned object must contain: - -- `state`: ioBroker state object +The returned object must be an ioBroker state object. This method can be implemented asynchronously and can take a lot of time to complete. @@ -325,7 +321,7 @@ This method returns a promise that resolves to a `ProgressDialog` object. - `update` (object): what to update in the dialog - `title` (string, optional): change the dialog title - `indeterminate` (boolean, optional): change whether the progress is indeterminate - - `value` (number, optional): change the progress value + - `value` (number, optional): change the progress value (if set, it must be a value between 0 and 100) - `label` (string, optional): change the label to the right of the progress bar - `close()` - Closes the progress dialog (and allows you to open other dialogs) @@ -366,6 +362,11 @@ class MyAdapterDeviceManagement extends DeviceManagement { ## Changelog +### **WORK IN PROGRESS** + +- (@UncleSamSwiss) Enabled incremental loading of devices +- (@UncleSamSwiss) Removed direct access to `DeviceManagement.handleXxx()` methods (use `handler` and similar properties instead) + ### 2.0.2 (2026-01-28) - (@GermanBluefox) BREAKING: Admin/GUI must have version 9 (or higher) of `dm-gui-components` diff --git a/examples/dm-test.ts b/examples/dm-test.ts index e53852c..cc72145 100644 --- a/examples/dm-test.ts +++ b/examples/dm-test.ts @@ -1,11 +1,10 @@ import { type ActionContext, type DeviceDetails, + type DeviceLoadContext, DeviceManagement, - type DeviceRefresh, type JsonFormSchema, } from '../src'; -import type * as base from '../src/types/base'; const demoFormSchema: JsonFormSchema = { type: 'tabs', @@ -84,69 +83,61 @@ export class DmTestDeviceManagement extends DeviceManagement { context.addDevice({ id: 'test-123', name: 'Test 123', status: 'connected' }); context.addDevice({ id: 'test-345', name: 'Test 345', status: 'disconnected', hasDetails: true, actions: [] }); context.addDevice({ - id: 'test-789', - name: 'Test 789', - status: 'connected', - actions: [ - { - id: 'play', - icon: 'fas fa-play', + id: 'test-789', + name: 'Test 789', + status: 'connected', + actions: [ + { + id: 'play', + icon: 'fas fa-play', + handler: (deviceId: string) => { + this.log.info(`Play was pressed on ${deviceId}`); + return { refresh: false }; }, - { - id: 'pause', - icon: 'fa-pause', - description: 'Pause device', - }, - { - id: 'forward', - icon: 'forward', - description: 'Forward', + }, + { + id: 'pause', + icon: 'fa-pause', + description: 'Pause device', + handler: async (deviceId: string, context: ActionContext) => { + this.log.info(`Pause was pressed on ${deviceId}`); + const confirm = await context.showConfirmation('Do you want to refresh the device only?'); + return { refresh: confirm ? 'device' : 'instance' }; }, - ], + }, + { + id: 'forward', + icon: 'forward', + description: 'Forward', + }, + ], }); context.addDevice({ - id: 'test-ABC', - name: 'Test ABC', - status: 'connected', - actions: [ - { - id: 'forms', - icon: 'fab fa-wpforms', - description: 'Show forms flow', + id: 'test-ABC', + name: 'Test ABC', + status: 'connected', + actions: [ + { + id: 'forms', + icon: 'fab fa-wpforms', + description: 'Show forms flow', + handler: async (deviceId: string, context: ActionContext) => { + this.log.info(`Forms was pressed on ${deviceId}`); + const data = await context.showForm(demoFormSchema, { + data: { myPort: 8081, secondPort: 8082 }, + }); + if (!data) { + await context.showMessage('You cancelled the previous form!'); + } else { + await context.showMessage(`You entered: ${JSON.stringify(data)}`); + } + return { refresh: false }; }, - ], + }, + ], }); } - protected override async handleDeviceAction( - deviceId: string, - actionId: string, - context: ActionContext, - ): Promise<{ refresh: DeviceRefresh }> { - switch (actionId) { - case 'play': - this.log.info(`Play was pressed on ${deviceId}`); - return { refresh: false }; - case 'pause': { - this.log.info(`Pause was pressed on ${deviceId}`); - const confirm = await context.showConfirmation('Do you want to refresh the device only?'); - return { refresh: confirm ? 'device' : 'instance' }; - } - case 'forms': { - this.log.info(`Forms was pressed on ${deviceId}`); - const data = await context.showForm(demoFormSchema, { data: { myPort: 8081, secondPort: 8082 } }); - if (!data) { - await context.showMessage('You cancelled the previous form!'); - } else { - await context.showMessage(`You entered: ${JSON.stringify(data)}`); - } - return { refresh: false }; - } - default: - throw new Error(`Unknown action ${actionId}`); - } - } - protected override getDeviceDetails(id: string): Promise { const schema: JsonFormSchema = { type: 'panel', diff --git a/src/DeviceManagement.ts b/src/DeviceManagement.ts index 3fc59c4..d7e6996 100644 --- a/src/DeviceManagement.ts +++ b/src/DeviceManagement.ts @@ -99,7 +99,7 @@ export abstract class DeviceManagement Date: Mon, 16 Feb 2026 11:46:17 +0100 Subject: [PATCH 10/23] Allow device ID to be any valid JSON Closes #12 --- README.md | 12 ++--- examples/dm-test.ts | 4 +- src/DeviceManagement.ts | 116 +++++++++++++++++++++------------------- src/types/adapter.ts | 7 +-- src/types/base.ts | 50 ++++++++--------- src/types/common.ts | 11 ++-- 6 files changed, 105 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index a9ea3cd..1641dc1 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ In most cases, you will get all states of your instance and fill the `context` w Every item is an object of type `DeviceInfo` which has the following properties: -- `id` (string): a unique (human-readable) identifier of the device (it must be unique for your adapter instance only) +- `id` (JSON object): a unique identifier of the device (it must be unique for your adapter instance only) - `name` (string or translations): the human-readable name of this device - `status` (optional): the current status of the device, which has to be an object containing: - `connection` (string): alowed values are: `"connected"` / `"disconnected"` @@ -161,13 +161,13 @@ If you override this method, the returned object must contain: - `handler` (function, optional): function that will be called when the user clicks on the button; if not given, the button will be disabled in the UI - `communicationStateId` (string) (optional): the ID of the state that is used by backend for communication with front-end (only API v2) -### `getDeviceDetails(id: string)` +### `getDeviceDetails(id: DeviceId)` This method is called if a device's `hasDetails` is set to `true` and the user clicks on the expander. The returned object must contain: -- `id` (string): the `id` given as parameter to the method call +- `id` (JSON object): the `id` given as parameter to the method call - `schema` (Custom JSON form schema): the schema of the Custom JSON form to show below the device information - `data` (object, optional): the data used to populate the Custom JSON form @@ -198,7 +198,7 @@ These methods are called when the user clicks on an action (i.e., button) for an The parameters of this function are: -- `deviceId` (string): the `id` of the device +- `deviceId` (JSON object): the `id` of the device - `context` (object): object containing helper methods that can be used when executing the action - `options` (object): object containing the action `value` (if given) @@ -219,7 +219,7 @@ These functions are called when the user clicks on a control (i.e., slider) in t The parameters of this method are: -- `deviceId` (string): the `id` that was given in `listDevices()` --> `[].id` +- `deviceId` (JSON object): the `id` that was given in `listDevices()` --> `[].id` - `controlId` (string): the `id` that was given in `listDevices()` --> `[].controls[].id`. There are some reserved control names, you can find the list below. - `newState` (string | number | boolean): new state for the control, that will be sent to a real device - `context` (object): object containing helper methods that can be used when executing the action @@ -234,7 +234,7 @@ These functions are called when GUI requests the update of the state. The parameters of this method are: -- `deviceId` (string): the `id` that was given in `listDevices()` --> `[].id` +- `deviceId` (JSON object): the `id` that was given in `listDevices()` --> `[].id` - `controlId` (string): the `id` that was given in `listDevices()` --> `[].controls[].id` - `context` (object): object containing helper methods that can be used when executing the action diff --git a/examples/dm-test.ts b/examples/dm-test.ts index cc72145..be7313f 100644 --- a/examples/dm-test.ts +++ b/examples/dm-test.ts @@ -79,7 +79,7 @@ const demoFormSchema: JsonFormSchema = { }; export class DmTestDeviceManagement extends DeviceManagement { - protected loadDevices(context: DeviceLoadContext): void { + protected loadDevices(context: DeviceLoadContext): void { context.addDevice({ id: 'test-123', name: 'Test 123', status: 'connected' }); context.addDevice({ id: 'test-345', name: 'Test 345', status: 'disconnected', hasDetails: true, actions: [] }); context.addDevice({ @@ -138,7 +138,7 @@ export class DmTestDeviceManagement extends DeviceManagement { }); } - protected override getDeviceDetails(id: string): Promise { + protected override getDeviceDetails(id: string): Promise> { const schema: JsonFormSchema = { type: 'panel', items: { diff --git a/src/DeviceManagement.ts b/src/DeviceManagement.ts index d7e6996..d32f375 100644 --- a/src/DeviceManagement.ts +++ b/src/DeviceManagement.ts @@ -6,6 +6,7 @@ import { type ActionBase, type ActionButton, type DeviceDetails, + type DeviceId, type DeviceInfo, type DeviceStatus, type ErrorResponse, @@ -18,21 +19,24 @@ import { import type * as api from './types/api'; import type { BackendToGuiCommand, ControlState, DeviceControl } from './types/base'; -export type DeviceLoadContext = { - addDevice(device: DeviceInfo): void; +export type DeviceLoadContext = { + addDevice(device: DeviceInfo): void; setTotalDevices(total: number): void; }; -export abstract class DeviceManagement { +export abstract class DeviceManagement< + TAdapter extends AdapterInstance = AdapterInstance, + TId extends DeviceId = string, +> { private instanceInfo?: InstanceDetails; - private devices?: Map; + private devices?: Map>; private readonly communicationStateId: string; - private readonly deviceLoadContexts = new Map(); - private readonly messageContexts = new Map(); + private readonly deviceLoadContexts = new Map>(); + private readonly messageContexts = new Map>(); constructor( - protected readonly adapter: T, + protected readonly adapter: TAdapter, communicationStateId?: string | boolean, ) { adapter.on('message', this.onMessage.bind(this)); @@ -68,7 +72,7 @@ export abstract class DeviceManagement { + protected async sendCommandToGui(command: BackendToGuiCommand): Promise { if (this.communicationStateId) { await this.adapter.setState(this.communicationStateId, JSON.stringify(command), true); } else { @@ -85,17 +89,17 @@ export abstract class DeviceManagement; + protected abstract loadDevices(context: DeviceLoadContext): RetVal; - protected getDeviceInfo(_deviceId: string): RetVal { + protected getDeviceInfo(_deviceId: TId): RetVal> { throw new Error('Do not send "infoUpdate" or "delete" command without implementing getDeviceInfo method!'); } - protected getDeviceStatus(_deviceId: string): RetVal { + protected getDeviceStatus(_deviceId: TId): RetVal { throw new Error('Do not send "statusUpdate" command without implementing getDeviceStatus method!'); } - protected getDeviceDetails(id: string): RetVal { + protected getDeviceDetails(id: TId): RetVal | null | { error: string }> { return { id, schema: {} as JsonFormSchema }; } @@ -136,7 +140,7 @@ export abstract class DeviceManagement a.id === actionId); if (!action) { - this.log.warn(`Device action ${actionId} doesn't exist on device ${deviceId}`); + this.log.warn(`Device action ${actionId} doesn't exist on device ${jsonId}`); return { error: { code: ErrorCodes.E_DEVICE_ACTION_UNKNOWN, - message: `Device action ${actionId} doesn't exist on device ${deviceId}`, + message: `Device action ${actionId} doesn't exist on device ${jsonId}`, }, }; } if (!action.handler) { - this.log.warn(`Device action ${actionId} on ${deviceId} is disabled because it has no handler`); + this.log.warn(`Device action ${actionId} on ${jsonId} is disabled because it has no handler`); return { error: { code: ErrorCodes.E_DEVICE_ACTION_NO_HANDLER, - message: `Device action ${actionId} on ${deviceId} is disabled because it has no handler`, + message: `Device action ${actionId} on ${jsonId} is disabled because it has no handler`, }, }; } @@ -185,10 +190,10 @@ export abstract class DeviceManagement, ): RetVal { if (!this.devices) { this.log.warn(`Device control ${controlId} was called before listDevices()`); @@ -199,33 +204,34 @@ export abstract class DeviceManagement a.id === controlId); if (!control) { - this.log.warn(`Device control ${controlId} doesn't exist on device ${deviceId}`); + this.log.warn(`Device control ${controlId} doesn't exist on device ${jsonId}`); return { error: { code: ErrorCodes.E_DEVICE_CONTROL_UNKNOWN, - message: `Device control ${controlId} doesn't exist on device ${deviceId}`, + message: `Device control ${controlId} doesn't exist on device ${jsonId}`, }, }; } if (!control.handler) { - this.log.warn(`Device control ${controlId} on ${deviceId} is disabled because it has no handler`); + this.log.warn(`Device control ${controlId} on ${jsonId} is disabled because it has no handler`); return { error: { code: ErrorCodes.E_DEVICE_CONTROL_NO_HANDLER, - message: `Device control ${controlId} on ${deviceId} is disabled because it has no handler`, + message: `Device control ${controlId} on ${jsonId} is disabled because it has no handler`, }, }; } @@ -235,9 +241,9 @@ export abstract class DeviceManagement, ): RetVal { if (!this.devices) { this.log.warn(`Device get state ${controlId} was called before listDevices()`); @@ -248,33 +254,34 @@ export abstract class DeviceManagement a.id === controlId); if (!control) { - this.log.warn(`Device get state ${controlId} doesn't exist on device ${deviceId}`); + this.log.warn(`Device get state ${controlId} doesn't exist on device ${jsonId}`); return { error: { code: ErrorCodes.E_DEVICE_GET_STATE_UNKNOWN, - message: `Device control ${controlId} doesn't exist on device ${deviceId}`, + message: `Device control ${controlId} doesn't exist on device ${jsonId}`, }, }; } if (!control.getStateHandler) { - this.log.warn(`Device get state ${controlId} on ${deviceId} is disabled because it has no handler`); + this.log.warn(`Device get state ${controlId} on ${jsonId} is disabled because it has no handler`); return { error: { code: ErrorCodes.E_DEVICE_GET_STATE_NO_HANDLER, - message: `Device get state ${controlId} on ${deviceId} is disabled because it has no handler`, + message: `Device get state ${controlId} on ${jsonId} is disabled because it has no handler`, }, }; } @@ -301,7 +308,7 @@ export abstract class DeviceManagement(msg, this.adapter); this.deviceLoadContexts.set(msg._id, context); await this.loadDevices(context); if (context.complete()) { @@ -309,16 +316,17 @@ export abstract class DeviceManagement { - if (map.has(value.id)) { - throw new Error(`Device ID ${value.id} is not unique`); + const jsonId = JSON.stringify(value.id); + if (map.has(jsonId)) { + throw new Error(`Device ID ${jsonId} is not unique`); } - map.set(value.id, value); + map.set(jsonId, value); return map; - }, new Map()); + }, new Map>()); return; } case 'dm:deviceInfo': { - const deviceInfo = await this.getDeviceInfo(msg.message as string); + const deviceInfo = await this.getDeviceInfo(msg.message as TId); this.sendReply( { ...deviceInfo, @@ -330,12 +338,12 @@ export abstract class DeviceManagement(deviceStatus, msg); return; } case 'dm:deviceDetails': { - const details = await this.getDeviceDetails(msg.message as string); + const details = await this.getDeviceDetails(msg.message as TId); this.sendReply(details, msg); return; } @@ -349,7 +357,7 @@ export abstract class DeviceManagement implements DeviceLoadContext { private readonly minBatchSize = 8; - public readonly devices: DeviceInfo[] = []; + public readonly devices: DeviceInfo[] = []; private readonly id: number; private sendNext: api.DeviceInfo[] = []; private totalDevices?: number; @@ -434,7 +442,7 @@ class DeviceLoadContextImpl implements DeviceLoadContext { this.id = msg._id; } - addDevice(device: DeviceInfo): void { + addDevice(device: DeviceInfo): void { this.devices.push(device); this.sendNext.push({ ...device, @@ -482,7 +490,7 @@ class DeviceLoadContextImpl implements DeviceLoadContext { } } -export class MessageContext implements ActionContext { +export class MessageContext implements ActionContext { private hasOpenProgressDialog = false; private lastMessage?: ioBroker.Message; private progressHandler?: (message: Record) => void; @@ -588,7 +596,7 @@ export class MessageContext implements ActionContext { }); } - sendControlResult(deviceId: string, controlId: string, result: ErrorResponse | ioBroker.State): void { + sendControlResult(deviceId: TId, controlId: string, result: ErrorResponse | ioBroker.State): void { if (typeof result === 'object' && 'error' in result) { this.send('result', { result: { diff --git a/src/types/adapter.ts b/src/types/adapter.ts index 40c616f..bd46908 100644 --- a/src/types/adapter.ts +++ b/src/types/adapter.ts @@ -1,8 +1,9 @@ import type * as base from './base'; +import type { DeviceId } from './common'; export type ActionBase = base.ActionBase<'adapter'>; export type InstanceAction = base.InstanceAction<'adapter'>; -export type DeviceAction = base.DeviceAction<'adapter'>; +export type DeviceAction = base.DeviceAction<'adapter', TId>; export type InstanceDetails = base.InstanceDetails<'adapter'>; -export type DeviceInfo = base.DeviceInfo<'adapter'>; -export type DeviceControl = base.DeviceControl<'adapter'>; +export type DeviceInfo = base.DeviceInfo<'adapter', TId>; +export type DeviceControl = base.DeviceControl<'adapter', TId>; diff --git a/src/types/base.ts b/src/types/base.ts index b6e90b9..fcbbefe 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -7,7 +7,7 @@ import type { ValueOrState, ValueOrStateOrObject, } from '..'; -import type { ApiVersion, DeviceRefresh, DeviceStatus, RetVal } from './common'; +import type { ApiVersion, DeviceId, DeviceStatus, RefreshResponse, RetVal } from './common'; type ActionType = 'api' | 'adapter'; export type Color = 'primary' | 'secondary' | (string & {}); // color (you can use primary, secondary or color rgb value or hex) @@ -118,18 +118,18 @@ export interface ControlBase { channel?: ChannelInfo; } -export interface DeviceControl extends ControlBase { - handler?: T extends 'api' +export interface DeviceControl extends ControlBase { + handler?: TType extends 'api' ? never : ( - deviceId: string, + deviceId: TId, actionId: string, state: ControlState, - context: MessageContext, + context: MessageContext, ) => RetVal; - getStateHandler?: T extends 'api' + getStateHandler?: TType extends 'api' ? never - : (deviceId: string, actionId: string, context: MessageContext) => RetVal; + : (deviceId: TId, actionId: string, context: MessageContext) => RetVal; } export interface InstanceAction extends ActionBase { @@ -139,14 +139,10 @@ export interface InstanceAction extends ActionBase title: ioBroker.StringOrTranslated; } -export interface DeviceAction extends ActionBase { +export interface DeviceAction extends ActionBase { handler?: T extends 'api' ? never - : ( - deviceId: string, - context: ActionContext, - options?: Record, - ) => RetVal<{ refresh: DeviceRefresh }>; + : (deviceId: TId, context: ActionContext, options?: Record) => RetVal; } export interface InstanceDetails { @@ -157,9 +153,9 @@ export interface InstanceDetails { communicationStateId?: string; } -export interface DeviceInfo { +export interface DeviceInfo { /** ID of the action. Should be unique only in one adapter. Other adapters could have same names */ - id: string; + id: TId; /** Name of the device. It will be shown in the card header */ name: ValueOrObject; /** base64 or url icon for device card */ @@ -176,9 +172,9 @@ export interface DeviceInfo { /** If this flag is true or false, the according indication will be shown. Additionally, if ACTIONS.ENABLE_DISABLE is implemented, this action will be sent to backend by clicking on this indication */ enabled?: ValueOrState; /** List of actions on the card */ - actions?: DeviceAction[]; + actions?: DeviceAction[]; /** List of controls on the card. The difference of controls and actions is that the controls can show status (e.g. on/off) and can work directly with states */ - controls?: DeviceControl[]; + controls?: DeviceControl[]; /** If true, the button `more` will be shown on the card and called `dm:deviceDetails` action to get the details */ hasDetails?: ValueOrStateOrObject; /** Device type for grouping */ @@ -190,28 +186,28 @@ export interface DeviceInfo { }; } -export interface BackendToGuiCommandDeviceInfoUpdate { +export interface BackendToGuiCommandDeviceInfoUpdate { /** Used for updating and for adding new device */ command: 'infoUpdate'; /** Device ID */ - deviceId: string; + deviceId: TId; /** Backend can send directly new information about device to avoid extra request from GUI */ info?: DeviceInfo; } -export interface BackendToGuiCommandDeviceStatusUpdate { +export interface BackendToGuiCommandDeviceStatusUpdate { /** Status of device was updated */ command: 'statusUpdate'; /** Device ID */ - deviceId: string; + deviceId: TId; /** Backend can send directly new status to avoid extra request from GUI */ status?: DeviceStatus; } -export interface BackendToGuiCommandDeviceDelete { +export interface BackendToGuiCommandDeviceDelete { /** Device was deleted */ command: 'delete'; - deviceId: string; + deviceId: TId; } export interface BackendToGuiCommandAllUpdate { @@ -219,8 +215,8 @@ export interface BackendToGuiCommandAllUpdate { command: 'all'; } -export type BackendToGuiCommand = - | BackendToGuiCommandDeviceInfoUpdate - | BackendToGuiCommandDeviceStatusUpdate - | BackendToGuiCommandDeviceDelete +export type BackendToGuiCommand = + | BackendToGuiCommandDeviceInfoUpdate + | BackendToGuiCommandDeviceStatusUpdate + | BackendToGuiCommandDeviceDelete | BackendToGuiCommandAllUpdate; diff --git a/src/types/common.ts b/src/types/common.ts index b59c23d..9a5d938 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -1,7 +1,12 @@ -export type ApiVersion = 'v1' | 'v2'; +export type ApiVersion = 'v2'; export type ConfigConnectionType = 'lan' | 'wifi' | 'bluetooth' | 'thread' | 'z-wave' | 'zigbee' | 'other'; +export interface ComplexDeviceId { + [key: string | number]: string | number | ComplexDeviceId; +} +export type DeviceId = string | number | ComplexDeviceId; + export type ValueOrObject = T | { objectId: string; property: string }; export type ValueOrState = T | { stateId: string; mapping?: M }; export type ValueOrStateOrObject = T | ValueOrObject | ValueOrState; @@ -1211,8 +1216,8 @@ export type JsonFormSchema = ConfigItemPanel | ConfigItemTabs; export type JsonFormData = Record; -export interface DeviceDetails { - id: string; +export interface DeviceDetails { + id: TId; schema: JsonFormSchema; data?: JsonFormData; } From 81414b5f54cfb07e78ab3eabc8127bff05a659d5 Mon Sep 17 00:00:00 2001 From: Samuel Weibel Date: Mon, 16 Feb 2026 12:01:45 +0100 Subject: [PATCH 11/23] Add identifier to device info Closes #13 --- README.md | 11 ++++++++--- examples/dm-test.ts | 13 +++++++++++-- src/types/base.ts | 6 +++++- src/types/common.ts | 11 +++++------ 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1641dc1..26fea36 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,7 @@ In most cases, you will get all states of your instance and fill the `context` w Every item is an object of type `DeviceInfo` which has the following properties: - `id` (JSON object): a unique identifier of the device (it must be unique for your adapter instance only) +- `identifier` (optional): a human-readable identifier of the device - `name` (string or translations): the human-readable name of this device - `status` (optional): the current status of the device, which has to be an object containing: - `connection` (string): alowed values are: `"connected"` / `"disconnected"` @@ -159,7 +160,8 @@ If you override this method, the returned object must contain: - `title` (string): the title shown next to the icon on the button - `description` (string, optional): a text that will be shown as a tooltip on the button - `handler` (function, optional): function that will be called when the user clicks on the button; if not given, the button will be disabled in the UI -- `communicationStateId` (string) (optional): the ID of the state that is used by backend for communication with front-end (only API v2) +- `communicationStateId` (string, optional): the ID of the state that is used by backend for communication with front-end +- `identifierLabel` (string or translations, optional): the human-readable label next to the identifier ### `getDeviceDetails(id: DeviceId)` @@ -175,9 +177,11 @@ For more details about the schema, see [here](https://github.com/ioBroker/ioBrok Please keep in mind that there is no "Save" button, so in most cases, the form shouldn't contain editable fields, but you may use `sendTo` objects to send data to the adapter. +## `DeviceManagement` handlers + ### InstanceInfo action handlers -These methods are called when the user clicks on an action (i.e., button) for an adapter instance. +These functions are called when the user clicks on an action (i.e., button) for an adapter instance. The parameters of this function are: @@ -194,7 +198,7 @@ See below for how to interact with the user. ### DeviceInfo action handlers -These methods are called when the user clicks on an action (i.e., button) for an adapter instance. +These functions are called when the user clicks on an action (i.e., button) for an adapter instance. The parameters of this function are: @@ -366,6 +370,7 @@ class MyAdapterDeviceManagement extends DeviceManagement { - (@UncleSamSwiss) Enabled incremental loading of devices - (@UncleSamSwiss) Removed direct access to `DeviceManagement.handleXxx()` methods (use `handler` and similar properties instead) +- (@UncleSamSwiss) Added `identifier` property to `DeviceInfo` for human-readable identifiers ### 2.0.2 (2026-01-28) diff --git a/examples/dm-test.ts b/examples/dm-test.ts index be7313f..723f53e 100644 --- a/examples/dm-test.ts +++ b/examples/dm-test.ts @@ -80,10 +80,18 @@ const demoFormSchema: JsonFormSchema = { export class DmTestDeviceManagement extends DeviceManagement { protected loadDevices(context: DeviceLoadContext): void { - context.addDevice({ id: 'test-123', name: 'Test 123', status: 'connected' }); - context.addDevice({ id: 'test-345', name: 'Test 345', status: 'disconnected', hasDetails: true, actions: [] }); + context.addDevice({ id: 'test-123', identifier: 'test-123', name: 'Test 123', status: 'connected' }); + context.addDevice({ + id: 'test-345', + identifier: 'test-345', + name: 'Test 345', + status: 'disconnected', + hasDetails: true, + actions: [], + }); context.addDevice({ id: 'test-789', + identifier: 'test-789', name: 'Test 789', status: 'connected', actions: [ @@ -114,6 +122,7 @@ export class DmTestDeviceManagement extends DeviceManagement { }); context.addDevice({ id: 'test-ABC', + identifier: 'test-ABC', name: 'Test ABC', status: 'connected', actions: [ diff --git a/src/types/base.ts b/src/types/base.ts index fcbbefe..e87a652 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -151,11 +151,15 @@ export interface InstanceDetails { actions?: InstanceAction[]; /** ID of state used for communication with GUI */ communicationStateId?: string; + /** Human-readable label next to the identifier */ + identifierLabel?: ioBroker.StringOrTranslated; } export interface DeviceInfo { - /** ID of the action. Should be unique only in one adapter. Other adapters could have same names */ + /** ID of the device. Must be be unique only in one adapter. Other adapters could have same IDs */ id: TId; + /** Human readable identifier of the device */ + identifier?: ValueOrObject; /** Name of the device. It will be shown in the card header */ name: ValueOrObject; /** base64 or url icon for device card */ diff --git a/src/types/common.ts b/src/types/common.ts index 9a5d938..61a312a 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -8,20 +8,19 @@ export interface ComplexDeviceId { export type DeviceId = string | number | ComplexDeviceId; export type ValueOrObject = T | { objectId: string; property: string }; -export type ValueOrState = T | { stateId: string; mapping?: M }; +export type ValueOrState = T | { stateId: string; mapping?: Record }; export type ValueOrStateOrObject = T | ValueOrObject | ValueOrState; export type DeviceStatus = | 'connected' | 'disconnected' | { - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents - battery?: ValueOrState; // in percent (0-100), or string 'charging' or value with unit as string, + battery?: ValueOrState; // in percent (0-100), or string 'charging' or value with unit as string, // or string '10V', // or string '10mV', // or string '100' in mV // or boolean true (means OK) or false (Battery warning) - connection?: ValueOrState<'connected' | 'disconnected', { [value: string]: 'connected' | 'disconnected' }>; + connection?: ValueOrState<'connected' | 'disconnected'>; rssi?: ValueOrState; // in dBm warning?: ValueOrState; // warning text or just boolean true (means warning) }; @@ -48,8 +47,8 @@ interface ObjectBrowserCustomFilter { common?: { type?: ioBroker.CommonType | ioBroker.CommonType[]; role?: string | string[]; - // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents - custom?: '_' | '_dataSources' | true | string | string[]; + + custom?: '_' | '_dataSources' | true | (string & {}) | string[]; }; } From 1245e4fbc168a22957a0be60cc6cf59adc2ceca9 Mon Sep 17 00:00:00 2001 From: Samuel Weibel Date: Mon, 16 Feb 2026 14:15:21 +0100 Subject: [PATCH 12/23] Allow to update and delete a device after an action Closes #15 --- README.md | 45 ++++++++++++++++++++++++++++++-------- examples/dm-test.ts | 35 ++++++++++++++++++++++++------ src/DeviceManagement.ts | 48 ++++++++++++++++++++++++++++------------- src/types/base.ts | 31 +++++++++++++++++++++++--- src/types/common.ts | 6 ------ 5 files changed, 126 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 26fea36..4539564 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Example: `this.getState()` -> `this.adapter.getState()` | 101 | Instance action ${actionId} was called before getInstanceInfo() was called. This could happen if the instance has restarted. | | 102 | Instance action ${actionId} is unknown. | | 103 | Instance action ${actionId} is disabled because it has no handler. | -| 201 | Device action ${actionId} was called before listDevices() was called. This could happen if the instance has restarted. | +| 201 | Device action ${actionId} was called before loadDevices() was called. This could happen if the instance has restarted. | | 202 | Device action ${actionId} was called on unknown device: ${deviceId}. | | 203 | Device action ${actionId} doesn't exist on device ${deviceId}. | | 204 | Device action ${actionId} on ${deviceId} is disabled because it has no handler. | @@ -119,6 +119,8 @@ This method must always be overridden (as it is abstract in the base class). You must fill the `context` with information about all devices of this adapter's instance. +You may call `context.setTotalDevices(count: number)` as soon as possible to let the GUI know how many devices in total will be loaded. This allows the GUI to show the loading progress. + This method is called when the user expands an instance in the list. In most cases, you will get all states of your instance and fill the `context` with the relevant information. @@ -223,8 +225,8 @@ These functions are called when the user clicks on a control (i.e., slider) in t The parameters of this method are: -- `deviceId` (JSON object): the `id` that was given in `listDevices()` --> `[].id` -- `controlId` (string): the `id` that was given in `listDevices()` --> `[].controls[].id`. There are some reserved control names, you can find the list below. +- `deviceId` (JSON object): the `id` that was given in `loadDevices()` --> `[].id` +- `controlId` (string): the `id` that was given in `loadDevices()` --> `[].controls[].id`. There are some reserved control names, you can find the list below. - `newState` (string | number | boolean): new state for the control, that will be sent to a real device - `context` (object): object containing helper methods that can be used when executing the action @@ -238,8 +240,8 @@ These functions are called when GUI requests the update of the state. The parameters of this method are: -- `deviceId` (JSON object): the `id` that was given in `listDevices()` --> `[].id` -- `controlId` (string): the `id` that was given in `listDevices()` --> `[].controls[].id` +- `deviceId` (JSON object): the `id` that was given in `loadDevices()` --> `[].id` +- `controlId` (string): the `id` that was given in `loadDevices()` --> `[].controls[].id` - `context` (object): object containing helper methods that can be used when executing the action The returned object must be an ioBroker state object. @@ -340,7 +342,7 @@ See example below: ```ts class MyAdapterDeviceManagement extends DeviceManagement { - protected listDevices(): RetVal { + protected loadDevices(context: DeviceLoadContext): void { const deviceInfo: DeviceInfo = { id: 'uniqieID', name: 'My device', @@ -354,23 +356,48 @@ class MyAdapterDeviceManagement extends DeviceManagement { }, hasDetails: true, }; - return [deviceInfo]; + context.addDevice(deviceInfo); } } ``` +## Migration from 1.x to 2.x + +Between version 1.x and 2.x, there are some breaking changes. Please also have a look at the changelog below for more information. + +### Incremental loading of devices + +In version 1.x, the `listDevices()` method had to return the full list of devices. +In version 2.x, this method was replaced by `loadDevices(context: DeviceLoadContext)` that allows incremental loading of devices. + +Instead of creating and returning an array of `DeviceInfo` objects, you have to call `context.addDevice(deviceInfo)` for each device you want to add to the list. + +You may also call `context.setTotalDevices(count: number)` as soon as possible to let the GUI know how many devices in total will be loaded. + +### Refresh response of device actions + +In version 2.x, the refresh response of device actions has changed. + +| Version 1.x | Version 2.x | Description | +| ------------ | ------------ | --------------------------------------------------------------------------- | +| `true` | `'all'` | the instance information as well as the entire device list will be reloaded | +| `false` | `'none'` | nothing will be reloaded | +| `'device'` | `'devices'` | the entire device list will be reloaded | +| `'instance'` | `'instance'` | (unchanged) only the instance information will be reloaded | + +## Changelog + -## Changelog - ### **WORK IN PROGRESS** - (@UncleSamSwiss) Enabled incremental loading of devices - (@UncleSamSwiss) Removed direct access to `DeviceManagement.handleXxx()` methods (use `handler` and similar properties instead) - (@UncleSamSwiss) Added `identifier` property to `DeviceInfo` for human-readable identifiers +- (@UncleSamSwiss) Device refresh responses can no longer be a `boolean` and `'device'` was renamed to `'devices'`. ### 2.0.2 (2026-01-28) diff --git a/examples/dm-test.ts b/examples/dm-test.ts index 723f53e..5a19d46 100644 --- a/examples/dm-test.ts +++ b/examples/dm-test.ts @@ -5,6 +5,7 @@ import { DeviceManagement, type JsonFormSchema, } from '../src'; +import { type DeviceRefreshResponse } from '../src/types/base'; const demoFormSchema: JsonFormSchema = { type: 'tabs', @@ -80,7 +81,29 @@ const demoFormSchema: JsonFormSchema = { export class DmTestDeviceManagement extends DeviceManagement { protected loadDevices(context: DeviceLoadContext): void { - context.addDevice({ id: 'test-123', identifier: 'test-123', name: 'Test 123', status: 'connected' }); + context.addDevice({ + id: 'test-123', + identifier: 'test-123', + name: 'Test 123', + status: 'connected', + actions: [ + { + id: 'update', + icon: 'forward', + handler: (deviceId: string): DeviceRefreshResponse<'adapter', string> => { + this.log.info(`Update was pressed on ${deviceId}`); + return { + update: { + id: 'test-123', + identifier: 'test-123 (*)', + name: `Updated name for ${deviceId}`, + status: 'disconnected', + }, + }; + }, + }, + ], + }); context.addDevice({ id: 'test-345', identifier: 'test-345', @@ -97,20 +120,20 @@ export class DmTestDeviceManagement extends DeviceManagement { actions: [ { id: 'play', - icon: 'fas fa-play', + icon: 'play', handler: (deviceId: string) => { this.log.info(`Play was pressed on ${deviceId}`); - return { refresh: false }; + return { refresh: 'none' }; }, }, { id: 'pause', - icon: 'fa-pause', + icon: 'pause', description: 'Pause device', handler: async (deviceId: string, context: ActionContext) => { this.log.info(`Pause was pressed on ${deviceId}`); const confirm = await context.showConfirmation('Do you want to refresh the device only?'); - return { refresh: confirm ? 'device' : 'instance' }; + return { refresh: confirm ? 'devices' : 'instance' }; }, }, { @@ -140,7 +163,7 @@ export class DmTestDeviceManagement extends DeviceManagement { } else { await context.showMessage(`You entered: ${JSON.stringify(data)}`); } - return { refresh: false }; + return { refresh: 'none' }; }, }, ], diff --git a/src/DeviceManagement.ts b/src/DeviceManagement.ts index d32f375..ba638f7 100644 --- a/src/DeviceManagement.ts +++ b/src/DeviceManagement.ts @@ -13,15 +13,20 @@ import { type InstanceDetails, type JsonFormData, type JsonFormSchema, - type RefreshResponse, type RetVal, } from './types'; import type * as api from './types/api'; -import type { BackendToGuiCommand, ControlState, DeviceControl } from './types/base'; +import type { + BackendToGuiCommand, + ControlState, + DeviceControl, + DeviceRefreshResponse, + InstanceRefreshResponse, +} from './types/base'; export type DeviceLoadContext = { addDevice(device: DeviceInfo): void; - setTotalDevices(total: number): void; + setTotalDevices(count: number): void; }; export abstract class DeviceManagement< @@ -107,7 +112,7 @@ export abstract class DeviceManagement< actionId: string, context?: ActionContext, options?: { value?: number | string | boolean; [key: string]: any }, - ): RetVal | RetVal { + ): RetVal | RetVal { if (!this.instanceInfo) { this.log.warn(`Instance action ${actionId} was called before getInstanceInfo()`); return { @@ -144,13 +149,13 @@ export abstract class DeviceManagement< actionId: string, context?: ActionContext, options?: { value?: number | string | boolean; [key: string]: any }, - ): RetVal | RetVal { + ): RetVal | RetVal> { if (!this.devices) { - this.log.warn(`Device action ${actionId} was called before listDevices()`); + this.log.warn(`Device action ${actionId} was called before loadDevices()`); return { error: { code: ErrorCodes.E_DEVICE_ACTION_NOT_INITIALIZED, - message: `Device action ${actionId} was called before listDevices()`, + message: `Device action ${actionId} was called before loadDevices()`, }, }; } @@ -196,11 +201,11 @@ export abstract class DeviceManagement< context?: MessageContext, ): RetVal { if (!this.devices) { - this.log.warn(`Device control ${controlId} was called before listDevices()`); + this.log.warn(`Device control ${controlId} was called before loadDevices()`); return { error: { code: ErrorCodes.E_DEVICE_CONTROL_NOT_INITIALIZED, - message: `Device control ${controlId} was called before listDevices()`, + message: `Device control ${controlId} was called before loadDevices()`, }, }; } @@ -246,11 +251,11 @@ export abstract class DeviceManagement< context?: MessageContext, ): RetVal { if (!this.devices) { - this.log.warn(`Device get state ${controlId} was called before listDevices()`); + this.log.warn(`Device get state ${controlId} was called before loadDevices()`); return { error: { code: ErrorCodes.E_DEVICE_GET_STATE_NOT_INITIALIZED, - message: `Device control ${controlId} was called before listDevices()`, + message: `Device control ${controlId} was called before loadDevices()`, }, }; } @@ -364,7 +369,20 @@ export abstract class DeviceManagement< value: action.value, }); this.messageContexts.delete(msg._id); - context.sendFinalResult(result); + if ('update' in result) { + // special handling for update responses (we need to update our cache and convert actions/controls before sending to GUI) + const update = result.update; + this.devices?.set(JSON.stringify(update.id), update); + context.sendFinalResult({ + update: { + ...update, + actions: convertActions(update.actions), + controls: convertControls(update.controls), + }, + }); + } else { + context.sendFinalResult(result); + } return; } case 'dm:deviceControl': { @@ -452,8 +470,8 @@ class DeviceLoadContextImpl implements DeviceLoadContext implements ActionContext { return promise; } - sendFinalResult(result: ErrorResponse | RefreshResponse): void { + sendFinalResult(result: ErrorResponse | DeviceRefreshResponse<'api', TId> | InstanceRefreshResponse): void { this.send('result', { result, }); diff --git a/src/types/base.ts b/src/types/base.ts index e87a652..aecf3c9 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -7,7 +7,7 @@ import type { ValueOrState, ValueOrStateOrObject, } from '..'; -import type { ApiVersion, DeviceId, DeviceStatus, RefreshResponse, RetVal } from './common'; +import type { ApiVersion, DeviceId, DeviceStatus, RetVal } from './common'; type ActionType = 'api' | 'adapter'; export type Color = 'primary' | 'secondary' | (string & {}); // color (you can use primary, secondary or color rgb value or hex) @@ -132,17 +132,42 @@ export interface DeviceControl) => RetVal; } +export type InstanceRefreshResponse = { + refresh: boolean; +}; + export interface InstanceAction extends ActionBase { handler?: T extends 'api' ? never - : (context: ActionContext, options?: Record) => RetVal<{ refresh: boolean }>; + : (context: ActionContext, options?: Record) => RetVal; title: ioBroker.StringOrTranslated; } +export type DeviceUpdate = { + update: DeviceInfo; +}; + +export type DeviceDelete = { + delete: TId; +}; + +export type DeviceRefresh = 'all' | 'devices' | 'instance' | 'none'; + +export type DeviceRefreshResponse = + | { + refresh: DeviceRefresh; + } + | DeviceUpdate + | DeviceDelete; + export interface DeviceAction extends ActionBase { handler?: T extends 'api' ? never - : (deviceId: TId, context: ActionContext, options?: Record) => RetVal; + : ( + deviceId: TId, + context: ActionContext, + options?: Record, + ) => RetVal>; } export interface InstanceDetails { diff --git a/src/types/common.ts b/src/types/common.ts index 61a312a..3f17e24 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -25,12 +25,6 @@ export type DeviceStatus = warning?: ValueOrState; // warning text or just boolean true (means warning) }; -export type DeviceRefresh = 'device' | 'instance' | false | true; - -export type RefreshResponse = { - refresh: DeviceRefresh; -}; - export type ErrorResponse = { error: { code: number; From e47a75945ffc9b827c8ec68aeaf1b15c416b8548 Mon Sep 17 00:00:00 2001 From: Samuel Weibel Date: Mon, 16 Feb 2026 14:36:40 +0100 Subject: [PATCH 13/23] Allow actions to be links Closes #11 --- README.md | 1 + examples/dm-test.ts | 22 ++++++++++++++++++++-- src/DeviceManagement.ts | 6 +++--- src/types/base.ts | 33 ++++++++++++++++++--------------- 4 files changed, 42 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 4539564..f8d3581 100644 --- a/README.md +++ b/README.md @@ -398,6 +398,7 @@ In version 2.x, the refresh response of device actions has changed. - (@UncleSamSwiss) Removed direct access to `DeviceManagement.handleXxx()` methods (use `handler` and similar properties instead) - (@UncleSamSwiss) Added `identifier` property to `DeviceInfo` for human-readable identifiers - (@UncleSamSwiss) Device refresh responses can no longer be a `boolean` and `'device'` was renamed to `'devices'`. +- (@UncleSamSwiss) Added `info` icon and possibility for actions to be a link (by providing a `url` property instead of a `handler` function) ### 2.0.2 (2026-01-28) diff --git a/examples/dm-test.ts b/examples/dm-test.ts index 5a19d46..2d8bfc5 100644 --- a/examples/dm-test.ts +++ b/examples/dm-test.ts @@ -110,7 +110,25 @@ export class DmTestDeviceManagement extends DeviceManagement { name: 'Test 345', status: 'disconnected', hasDetails: true, - actions: [], + actions: [ + { + id: 'info', + icon: 'settings', + // instead of handler, url can be provided to open a link when action is clicked + url: 'https://www.iobroker.net', + }, + { + id: 'infoTranslated', + icon: 'info', + // The URL can also be translated, so it can be different for different languages + url: { + en: 'https://www.iobroker.net/#en', + de: 'https://www.iobroker.net/#de', + ru: 'https://www.iobroker.net/#ru', + 'zh-cn': 'https://www.iobroker.net/#zh-cn', + }, + }, + ], }); context.addDevice({ id: 'test-789', @@ -132,7 +150,7 @@ export class DmTestDeviceManagement extends DeviceManagement { description: 'Pause device', handler: async (deviceId: string, context: ActionContext) => { this.log.info(`Pause was pressed on ${deviceId}`); - const confirm = await context.showConfirmation('Do you want to refresh the device only?'); + const confirm = await context.showConfirmation('Do you want to refresh the device list only?'); return { refresh: confirm ? 'devices' : 'instance' }; }, }, diff --git a/src/DeviceManagement.ts b/src/DeviceManagement.ts index ba638f7..08022ce 100644 --- a/src/DeviceManagement.ts +++ b/src/DeviceManagement.ts @@ -132,7 +132,7 @@ export abstract class DeviceManagement< }, }; } - if (!action.handler) { + if (!('handler' in action) || !action.handler) { this.log.warn(`Instance action ${actionId} is disabled because it has no handler`); return { error: { @@ -181,7 +181,7 @@ export abstract class DeviceManagement< }, }; } - if (!action.handler) { + if (!('handler' in action) || !action.handler) { this.log.warn(`Device action ${actionId} on ${jsonId} is disabled because it has no handler`); return { error: { @@ -688,7 +688,7 @@ function convertActions(actions? }); // remove handler function to send it as JSON - return actions.map((a: any) => ({ ...a, handler: undefined, disabled: !a.handler })); + return actions.map((a: any) => ({ ...a, handler: undefined, disabled: !a.handler && !a.url })); } function convertControls, U extends DeviceControl<'api'>>( diff --git a/src/types/base.ts b/src/types/base.ts index aecf3c9..56944ac 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -57,6 +57,7 @@ export interface ActionBase { | 'users' | 'group' | 'user' + | 'info' | (string & {}); // base64 or url description?: ioBroker.StringOrTranslated; disabled?: T extends 'api' ? boolean : never; @@ -136,12 +137,14 @@ export type InstanceRefreshResponse = { refresh: boolean; }; -export interface InstanceAction extends ActionBase { - handler?: T extends 'api' - ? never - : (context: ActionContext, options?: Record) => RetVal; - title: ioBroker.StringOrTranslated; -} +export type WithHandlerOrUrl = + | { handler?: TType extends 'api' ? never : THandler } + | { url: ioBroker.StringOrTranslated }; + +export type InstanceAction = ActionBase & + WithHandlerOrUrl) => RetVal> & { + title: ioBroker.StringOrTranslated; + }; export type DeviceUpdate = { update: DeviceInfo; @@ -160,15 +163,15 @@ export type DeviceRefreshResponse | DeviceDelete; -export interface DeviceAction extends ActionBase { - handler?: T extends 'api' - ? never - : ( - deviceId: TId, - context: ActionContext, - options?: Record, - ) => RetVal>; -} +export type DeviceAction = ActionBase & + WithHandlerOrUrl< + T, + ( + deviceId: TId, + context: ActionContext, + options?: Record, + ) => RetVal> + >; export interface InstanceDetails { /** API Version: 1 - till 2025 (including), 2 - from 2026 */ From 23dab75304873b06a3f2bef473d25f2b2d3a8b3f Mon Sep 17 00:00:00 2001 From: Samuel Weibel Date: Mon, 16 Feb 2026 17:45:26 +0100 Subject: [PATCH 14/23] Remove requirement to add common.supportedMessages.custom But also make clear that common.messagebox should be replaced by common.supportedMessages.custom --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f8d3581..515ddd6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Utility classes for ioBroker adapters to support [ioBroker.device-manager](https Add in your `io-package.json` the property `deviceManager: true` to `common.supportedMessages`. Note: If you don't have a `common.supportedMessages` property yet, you have to add it. -Also, if you don't have a `common.supportedMessages.custom: true` property yet, you have to add it. If common.messagebox exists, you can remove it. (see +Also, if you have a `common.messagebox` property, you can remove it and add `common.supportedMessages.custom: true`. (see https://github.com/ioBroker/ioBroker.js-controller/blob/274f9e8f84dbdaaba9830a6cc00ddf083e989090/schemas/io-package.json#L754C104-L754C178) In your ioBroker adapter, add a subclass of `DeviceManagement` and override the methods you need (see next chapters): From 47e07e5425b3a2b3829af8c70e2a6cc23ac9686d Mon Sep 17 00:00:00 2001 From: Samuel Weibel Date: Wed, 18 Feb 2026 15:54:55 +0000 Subject: [PATCH 15/23] Add response types to the API exports --- src/types/api.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types/api.ts b/src/types/api.ts index 855f426..59c6262 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -6,6 +6,8 @@ export type DeviceAction = base.DeviceAction<'api'>; export type InstanceDetails = base.InstanceDetails<'api'>; export type DeviceInfo = base.DeviceInfo<'api'>; export type DeviceControl = base.DeviceControl<'api'>; +export type DeviceRefreshResponse = base.DeviceRefreshResponse<'api'>; +export type InstanceRefreshResponse = base.InstanceRefreshResponse; export type DeviceLoadIncrement = { add: DeviceInfo[]; From 1078cd779fa1aa4d4c104a52ddb5e281058d718a Mon Sep 17 00:00:00 2001 From: Samuel Weibel Date: Wed, 25 Feb 2026 10:06:04 +0000 Subject: [PATCH 16/23] Fixing API confusion with better types Mainly, progress was completely wrong on the client side because types were not reusable --- src/ActionContext.ts | 7 ++--- src/DeviceManagement.ts | 58 ++++++++++++++----------------------- src/ProgressDialog.ts | 9 ++---- src/types/api.ts | 64 +++++++++++++++++++++++++++++++++++++++++ src/types/common.ts | 10 +++++++ 5 files changed, 101 insertions(+), 47 deletions(-) diff --git a/src/ActionContext.ts b/src/ActionContext.ts index fc3c511..8ca6e32 100644 --- a/src/ActionContext.ts +++ b/src/ActionContext.ts @@ -1,12 +1,9 @@ -import type { BackEndCommandJsonFormOptions, JsonFormData, JsonFormSchema } from '.'; +import type { BackEndCommandJsonFormOptions, JsonFormData, JsonFormSchema, ProgressOptions } from '.'; import type { ProgressDialog } from './ProgressDialog'; export interface ActionContext { showMessage(text: ioBroker.StringOrTranslated): Promise; showConfirmation(text: ioBroker.StringOrTranslated): Promise; showForm(schema: JsonFormSchema, options?: BackEndCommandJsonFormOptions): Promise; - openProgress( - title: string, - options?: { indeterminate?: boolean; value?: number; label?: ioBroker.StringOrTranslated }, - ): Promise; + openProgress(title: string, options?: ProgressOptions): Promise; } diff --git a/src/DeviceManagement.ts b/src/DeviceManagement.ts index 08022ce..fdac056 100644 --- a/src/DeviceManagement.ts +++ b/src/DeviceManagement.ts @@ -23,12 +23,16 @@ import type { DeviceRefreshResponse, InstanceRefreshResponse, } from './types/base'; +import type { ProgressOptions, ProgressUpdate } from './types/common'; export type DeviceLoadContext = { addDevice(device: DeviceInfo): void; setTotalDevices(count: number): void; }; +// Based on https://tkdodo.eu/blog/omit-for-discriminated-unions-in-type-script +type DistributiveOmit = T extends any ? Omit : never; + export abstract class DeviceManagement< TAdapter extends AdapterInstance = AdapterInstance, TId extends DeviceId = string, @@ -525,9 +529,7 @@ export class MessageContext implements ActionContext { const promise = new Promise(resolve => { this.progressHandler = () => resolve(); }); - this.send('message', { - message: text, - }); + this.send({ type: 'message', message: text }); return promise; } @@ -536,9 +538,7 @@ export class MessageContext implements ActionContext { const promise = new Promise(resolve => { this.progressHandler = msg => resolve(!!msg.confirm); }); - this.send('confirm', { - confirm: text, - }); + this.send({ type: 'confirm', confirm: text }); return promise; } @@ -554,30 +554,22 @@ export class MessageContext implements ActionContext { const promise = new Promise(resolve => { this.progressHandler = msg => resolve(msg.data); }); - this.send('form', { + this.send({ + type: 'form', form: { schema, ...options }, }); return promise; } - openProgress( - title: string, - options?: { indeterminate?: boolean; value?: number; label?: string }, - ): Promise { + openProgress(title: ioBroker.StringOrTranslated, options?: ProgressOptions): Promise { this.checkPreconditions(); this.hasOpenProgressDialog = true; const dialog: ProgressDialog = { - update: (update: { title?: string; indeterminate?: boolean; value?: number; label?: string }) => { + update: (update: ProgressUpdate) => { const promise = new Promise(resolve => { this.progressHandler = () => resolve(); }); - this.send( - 'progress', - { - progress: { title, ...options, ...update, open: true }, - }, - true, - ); + this.send({ type: 'progress', progress: update }, true); return promise; }, @@ -588,9 +580,7 @@ export class MessageContext implements ActionContext { resolve(); }; }); - this.send('progress', { - progress: { open: false }, - }); + this.send({ type: 'progress', progress: { open: false } }); return promise; }, }; @@ -598,25 +588,18 @@ export class MessageContext implements ActionContext { const promise = new Promise(resolve => { this.progressHandler = () => resolve(dialog); }); - this.send( - 'progress', - { - progress: { title, ...options, open: true }, - }, - true, - ); + this.send({ type: 'progress', progress: { title, ...options, open: true } }, true); return promise; } sendFinalResult(result: ErrorResponse | DeviceRefreshResponse<'api', TId> | InstanceRefreshResponse): void { - this.send('result', { - result, - }); + this.send({ type: 'result', result }); } sendControlResult(deviceId: TId, controlId: string, result: ErrorResponse | ioBroker.State): void { if (typeof result === 'object' && 'error' in result) { - this.send('result', { + this.send({ + type: 'result', result: { error: result.error, deviceId, @@ -624,7 +607,8 @@ export class MessageContext implements ActionContext { }, }); } else { - this.send('result', { + this.send({ + type: 'result', result: { state: result, deviceId, @@ -651,7 +635,10 @@ export class MessageContext implements ActionContext { } } - private send(type: string, message: any, doNotClose?: boolean): void { + private send( + message: DistributiveOmit, + doNotClose?: boolean, + ): void { if (!this.lastMessage) { throw new Error("No outstanding message, can't send a new one"); } @@ -660,7 +647,6 @@ export class MessageContext implements ActionContext { this.lastMessage.command, { ...message, - type, origin: this.lastMessage.message.origin || this.lastMessage._id, }, this.lastMessage.callback, diff --git a/src/ProgressDialog.ts b/src/ProgressDialog.ts index db2277f..fe26760 100644 --- a/src/ProgressDialog.ts +++ b/src/ProgressDialog.ts @@ -1,10 +1,7 @@ +import type { ProgressUpdate } from './types/common'; + export interface ProgressDialog { - update(update: { - title?: ioBroker.StringOrTranslated; - indeterminate?: boolean; - value?: number; - label?: ioBroker.StringOrTranslated; - }): Promise; + update(update: ProgressUpdate): Promise; close(): Promise; } diff --git a/src/types/api.ts b/src/types/api.ts index 59c6262..37d1976 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -1,4 +1,5 @@ import type * as base from './base'; +import type { ActionButton, DeviceId, ErrorResponse, JsonFormSchema, ProgressUpdate } from './common'; export type ActionBase = base.ActionBase<'api'>; export type InstanceAction = base.InstanceAction<'api'>; @@ -14,3 +15,66 @@ export type DeviceLoadIncrement = { total?: number; next?: { origin: number }; }; + +export type DmResponseBase = { + origin: number; +}; + +export type DmControlResponse = DmResponseBase & { + type: 'result'; + result: { + deviceId: DeviceId; + controlId: string; + } & ( + | ErrorResponse + | { + state: ioBroker.State; + } + ); +}; + +export type DmActionResultResponse = DmResponseBase & { + type: 'result'; + result: ErrorResponse | DeviceRefreshResponse | InstanceRefreshResponse; +}; + +export type DmActionMessageResponse = DmResponseBase & { + type: 'message'; + message: ioBroker.StringOrTranslated; +}; + +export type DmActionConfirmResponse = DmResponseBase & { + type: 'confirm'; + confirm: ioBroker.StringOrTranslated; +}; + +export interface CommunicationForm { + title?: ioBroker.StringOrTranslated | null | undefined; + label?: ioBroker.StringOrTranslated | null | undefined; // same as title + noTranslation?: boolean; // Do not translate title/label + schema: JsonFormSchema; + data?: Record; + buttons?: (ActionButton | 'apply' | 'cancel' | 'close')[]; + maxWidth?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + /** Minimal width of the dialog */ + minWidth?: number; + /** Always allow the apply button. Even when nothing was changed */ + ignoreApplyDisabled?: boolean; +} + +export type DmActionFormResponse = DmResponseBase & { + type: 'form'; + form: CommunicationForm; +}; + +export type DmActionProgressResponse = DmResponseBase & { + type: 'progress'; + progress: ProgressUpdate & { open?: boolean }; +}; + +export type DmActionResponse = + | DmActionResultResponse + | DmActionMessageResponse + | DmActionConfirmResponse + | DmActionFormResponse + | DmActionProgressResponse; diff --git a/src/types/common.ts b/src/types/common.ts index 3f17e24..4326eb6 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -34,6 +34,16 @@ export type ErrorResponse = { export type RetVal = T | Promise; +export interface ProgressOptions { + indeterminate?: boolean; + value?: number; + label?: ioBroker.StringOrTranslated; +} + +export interface ProgressUpdate extends ProgressOptions { + title?: ioBroker.StringOrTranslated; +} + type CustomCSSProperties = Record; interface ObjectBrowserCustomFilter { From c326310948acc1544264396c403923f40d8f8e7f Mon Sep 17 00:00:00 2001 From: GermanBluefox Date: Wed, 25 Feb 2026 11:38:33 +0100 Subject: [PATCH 17/23] Added required types export --- src/types/index.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/types/index.ts b/src/types/index.ts index 052823c..b83243a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,15 @@ export type * from './adapter'; -export { ACTIONS, type ChannelInfo, type Color, type ControlBase, type ControlState } from './base'; +export { + ACTIONS, + type ChannelInfo, + type Color, + type ControlBase, + type ControlState, + type BackendToGuiCommand, + type BackendToGuiCommandAllUpdate, + type BackendToGuiCommandDeviceDelete, + type BackendToGuiCommandDeviceStatusUpdate, + type BackendToGuiCommandDeviceInfoUpdate, +} from './base'; export type * from './common'; export * from './errorCodes'; From be0b53b7d23433587f8123ec33d7f57681b77740 Mon Sep 17 00:00:00 2001 From: GermanBluefox Date: Wed, 25 Feb 2026 11:57:59 +0100 Subject: [PATCH 18/23] MOved tasks.js to TS --- package.json | 2 +- src/types/base.ts | 4 ++-- tasks.js => tasks.ts | 30 ++++++++++++++++-------------- 3 files changed, 19 insertions(+), 17 deletions(-) rename tasks.js => tasks.ts (65%) diff --git a/package.json b/package.json index 6724dad..1ed21be 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "build": "tsc -p tsconfig.json", "test:package": "node -e \"process.exit(0)\"", "lint": "eslint -c eslint.config.mjs src", - "updateCommonTs": "node tasks.js", + "updateCommonTs": "npx -y tsx tasks.ts", "prettier": "prettier -u -w examples src", "release": "release-script", "release-patch": "release-script patch --yes", diff --git a/src/types/base.ts b/src/types/base.ts index 56944ac..637a093 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -184,9 +184,9 @@ export interface InstanceDetails { } export interface DeviceInfo { - /** ID of the device. Must be be unique only in one adapter. Other adapters could have same IDs */ + /** ID of the device. Must be unique only in one adapter. Other adapters could have same IDs */ id: TId; - /** Human readable identifier of the device */ + /** Human-readable identifier of the device */ identifier?: ValueOrObject; /** Name of the device. It will be shown in the card header */ name: ValueOrObject; diff --git a/tasks.js b/tasks.ts similarity index 65% rename from tasks.js rename to tasks.ts index 241b16e..0e2ac50 100644 --- a/tasks.js +++ b/tasks.ts @@ -1,16 +1,17 @@ -const { readFileSync, writeFileSync } = require('node:fs'); -const axios = require('axios'); -const COMMON_FILENAME = `${__dirname}/src/types/common.ts`; -const TYPES_URL = 'https://raw.githubusercontent.com/ioBroker/json-config/main/src/types.d.ts'; - -async function patchCommonTs() { - let text = readFileSync(COMMON_FILENAME).toString(); - const response = await axios.get(TYPES_URL); - const typeLines = response.data.toString().split('\n'); - - const lines = text.split('\n'); - const start = []; - const end = []; +import { readFileSync, writeFileSync } from 'node:fs'; +import axios from 'axios'; + +const COMMON_FILENAME: string = `${process.cwd()}/src/types/common.ts`; +const TYPES_URL: string = 'https://raw.githubusercontent.com/ioBroker/json-config/main/src/types.d.ts'; + +async function patchCommonTs(): Promise { + const text: string = readFileSync(COMMON_FILENAME, 'utf-8'); + const response = await axios.get(TYPES_URL); + const typeLines: string[] = response.data.split('\n'); + + const lines: string[] = text.split('\n'); + const start: string[] = []; + const end: string[] = []; let found = 0; for (let i = 0; i < lines.length; i++) { if (lines[i].startsWith('// -- START OF DYNAMIC GENERATED CODE')) { @@ -53,4 +54,5 @@ async function patchCommonTs() { writeFileSync(COMMON_FILENAME, `${start.join('\n')}\n${typeLines.join('\n')}\n${end.join('\n')}`); } -patchCommonTs().catch(err => console.error(`Error: ${err}`)); +patchCommonTs().catch((err: unknown) => console.error(`Error: ${err}`)); + From 7595bac38d7465b81a9803e39aaddd60745ed96b Mon Sep 17 00:00:00 2001 From: GermanBluefox Date: Wed, 25 Feb 2026 12:35:31 +0100 Subject: [PATCH 19/23] Added README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 515ddd6..9a3fd19 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Utility classes for ioBroker adapters to support [ioBroker.device-manager](https Add in your `io-package.json` the property `deviceManager: true` to `common.supportedMessages`. Note: If you don't have a `common.supportedMessages` property yet, you have to add it. -Also, if you have a `common.messagebox` property, you can remove it and add `common.supportedMessages.custom: true`. (see +Also, if you have a `common.messagebox` property for the adapter specific messages, you can remove it and add `common.supportedMessages.custom: true`. (see https://github.com/ioBroker/ioBroker.js-controller/blob/274f9e8f84dbdaaba9830a6cc00ddf083e989090/schemas/io-package.json#L754C104-L754C178) In your ioBroker adapter, add a subclass of `DeviceManagement` and override the methods you need (see next chapters): From 3bec16f812799f806e92f273df970da1d973a47d Mon Sep 17 00:00:00 2001 From: GermanBluefox Date: Wed, 25 Feb 2026 12:41:04 +0100 Subject: [PATCH 20/23] Formatting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9a3fd19..b95e390 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ Every item is an object of type `DeviceInfo` which has the following properties: - `connection` (string): alowed values are: `"connected"` / `"disconnected"` - `rssi` (number): rssi value of the connection - `battery` (boolean / number): if boolean: false: Battery empty. If number: battery level of the device (shows also a battery symbol at card) - - `warning` (boolean / string): if boolean: true indicates a warning. If string: shows also the warning with mouseover + - `warning` (boolean / string): if boolean: true indicates a warning. If a string: shows also the warning with mouseover - `actions` (array, optional): an array of actions that can be performed on the device; each object contains: - `id` (string): unique identifier to recognize an action (never shown to the user) - `icon` (string): an icon shown on the button (see below for details) From e6810911951a7b9627d9616f9487ab73094f01b5 Mon Sep 17 00:00:00 2001 From: Samuel Weibel Date: Wed, 25 Feb 2026 13:33:05 +0000 Subject: [PATCH 21/23] Change protocol version for this PR to v3 v2 is already used by matter@1.0.0 and we would cause breaking API changes --- README.md | 2 +- src/DeviceManagement.ts | 2 +- src/types/common.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b95e390..132ed99 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ This method allows the device manager tab to gather some general information abo If you override this method, the returned object must contain: -- `apiVersion` (string): the supported API version; must be `"v1"` or `"v2"` (if "backend to GUI communication" is used or IDs instead of values) +- `apiVersion` (string): the supported API version; must be `"v3"` - `actions` (array, optional): an array of actions that can be performed on the instance; each object contains: - `id` (string): unique identifier to recognize an action (never shown to the user) - `icon` (string): an icon shown on the button (see below for details) diff --git a/src/DeviceManagement.ts b/src/DeviceManagement.ts index fdac056..822a0fe 100644 --- a/src/DeviceManagement.ts +++ b/src/DeviceManagement.ts @@ -95,7 +95,7 @@ export abstract class DeviceManagement< protected getInstanceInfo(): RetVal { // Overload this method if your adapter does not use BackendToGui communication and States/Objects in DeviceInfo - return { apiVersion: 'v2', communicationStateId: this.communicationStateId || undefined }; + return { apiVersion: 'v3', communicationStateId: this.communicationStateId || undefined }; } protected abstract loadDevices(context: DeviceLoadContext): RetVal; diff --git a/src/types/common.ts b/src/types/common.ts index 4326eb6..4fe018f 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -1,4 +1,4 @@ -export type ApiVersion = 'v2'; +export type ApiVersion = 'v3'; export type ConfigConnectionType = 'lan' | 'wifi' | 'bluetooth' | 'thread' | 'z-wave' | 'zigbee' | 'other'; From 6c2eca2ea77674afbfeb0bc21a8aeaabfd6ac1a9 Mon Sep 17 00:00:00 2001 From: GermanBluefox Date: Wed, 25 Feb 2026 15:19:53 +0100 Subject: [PATCH 22/23] Formatting --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 132ed99..05eb2a2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Utility classes for ioBroker adapters to support [ioBroker.device-manager](https Add in your `io-package.json` the property `deviceManager: true` to `common.supportedMessages`. Note: If you don't have a `common.supportedMessages` property yet, you have to add it. -Also, if you have a `common.messagebox` property for the adapter specific messages, you can remove it and add `common.supportedMessages.custom: true`. (see +Also, if you have a `common.messagebox` property for the adapter-specific messages, you can remove it and add `common.supportedMessages.custom: true`. (see https://github.com/ioBroker/ioBroker.js-controller/blob/274f9e8f84dbdaaba9830a6cc00ddf083e989090/schemas/io-package.json#L754C104-L754C178) In your ioBroker adapter, add a subclass of `DeviceManagement` and override the methods you need (see next chapters): @@ -62,7 +62,7 @@ the `DeviceManagement` implementation's `handleXxxAction()` is called, and the a ### Controls -The device manager tab allows the user to control devices too. If devices are controllable, the device manager tab shows a control elements in the device card. +The device manager tab allows the user to control devices too. If devices are controllable, the device manager tab shows the control elements in the device card. When the user clicks on a control (i.e., a button in the UI), the `DeviceManagement` implementation's `handleXxxAction()` is called, and the adapter can perform arbitrary actions @@ -74,7 +74,7 @@ The communication between the `ioBroker.device-manager` tab and the adapter happ **IMPORTANT:** make sure your adapter doesn't handle `sendTo` messages starting with `dm:`, otherwise the communication will not work. -- Use for Example this on the top of your onMessage Methode: +- Use, for example, this on the top of your onMessage Methode: ```js if (obj.command?.startsWith('dm:')) { @@ -133,7 +133,7 @@ Every item is an object of type `DeviceInfo` which has the following properties: - `status` (optional): the current status of the device, which has to be an object containing: - `connection` (string): alowed values are: `"connected"` / `"disconnected"` - `rssi` (number): rssi value of the connection - - `battery` (boolean / number): if boolean: false: Battery empty. If number: battery level of the device (shows also a battery symbol at card) + - `battery` (boolean / number): if boolean: false - the battery is empty. If number: the battery level of the device (shows also a battery symbol on the card) - `warning` (boolean / string): if boolean: true indicates a warning. If a string: shows also the warning with mouseover - `actions` (array, optional): an array of actions that can be performed on the device; each object contains: - `id` (string): unique identifier to recognize an action (never shown to the user) @@ -313,7 +313,7 @@ The method has the following parameters: - `options` (object, optional): options to configure the dialog further - `indeterminate` (boolean, optional): set to `true` to visualize an unspecified wait time - `value` (number, optional): the progress value to show to the user (if set, it must be a value between 0 and 100) - - `label` (string, optional): label to show to the right of the progress bar; you may show the progress value in a human-readable way (e.g. "42%") or show the current step in a multi-step progress (e.g. "Logging in...") + - `label` (string, optional): the label to show to the right of the progress bar; you may show the progress value in a human-readable way (e.g. "42%") or show the current step in multi-step progress (e.g. "Logging in...") This method returns a promise that resolves to a `ProgressDialog` object. @@ -334,11 +334,11 @@ This method returns a promise that resolves to a `ProgressDialog` object. ### `sendCommandToGui(command: BackendToGuiCommand)` -Sends command to GUI to add/update/delete devices or to update the status of device. +Sends command to GUI to add/update/delete devices or to update the status of a device. -**It is suggested** to use the state's ID directly in DeviceInfo structure instead of sending the command every time to GUI on status update. +**It is suggested** to use the state's ID directly in the DeviceInfo structure instead of sending the command every time to GUI on status update. -See example below: +See the example below: ```ts class MyAdapterDeviceManagement extends DeviceManagement { @@ -363,7 +363,7 @@ class MyAdapterDeviceManagement extends DeviceManagement { ## Migration from 1.x to 2.x -Between version 1.x and 2.x, there are some breaking changes. Please also have a look at the changelog below for more information. +Between versions 1.x and 2.x, there are some breaking changes. Please also have a look at the changelog below for more information. ### Incremental loading of devices @@ -403,7 +403,7 @@ In version 2.x, the refresh response of device actions has changed. ### 2.0.2 (2026-01-28) - (@GermanBluefox) BREAKING: Admin/GUI must have version 9 (or higher) of `dm-gui-components` -- (@GermanBluefox) Added types to update status of device directly from state +- (@GermanBluefox) Added types to update the status of a device directly from the state - (@GermanBluefox) Added backend to GUI communication possibility - (@GermanBluefox) Added `dm:deviceInfo` command - (@GermanBluefox) Added `dm:deviceStatus` command @@ -441,7 +441,7 @@ In version 2.x, the refresh response of device actions has changed. ### 1.0.0 (2025-01-08) - (@GermanBluefox) Added `disabled` options for a device -- (@GermanBluefox) Major release just because is good enough. No breaking changes. +- (@GermanBluefox) Major release just because it is good enough. No breaking changes. ### 0.6.11 (2024-12-11) From ae5b5a51ac897b900dd912602ef9fc81104f6511 Mon Sep 17 00:00:00 2001 From: GermanBluefox Date: Thu, 26 Feb 2026 10:15:30 +0100 Subject: [PATCH 23/23] Adding build folder to git --- .gitignore | 1 - build/ActionContext.d.ts | 8 + build/ActionContext.js | 2 + build/DeviceManagement.d.ts | 56 +++ build/DeviceManagement.js | 540 ++++++++++++++++++++ build/ProgressDialog.d.ts | 5 + build/ProgressDialog.js | 2 + build/index.d.ts | 3 + build/index.js | 19 + build/types/adapter.d.ts | 8 + build/types/adapter.js | 2 + build/types/api.d.ts | 65 +++ build/types/api.js | 2 + build/types/base.d.ts | 178 +++++++ build/types/base.js | 10 + build/types/common.d.ts | 951 ++++++++++++++++++++++++++++++++++++ build/types/common.js | 2 + build/types/errorCodes.d.ts | 17 + build/types/errorCodes.js | 21 + build/types/index.d.ts | 4 + build/types/index.js | 20 + 21 files changed, 1915 insertions(+), 1 deletion(-) create mode 100644 build/ActionContext.d.ts create mode 100644 build/ActionContext.js create mode 100644 build/DeviceManagement.d.ts create mode 100644 build/DeviceManagement.js create mode 100644 build/ProgressDialog.d.ts create mode 100644 build/ProgressDialog.js create mode 100644 build/index.d.ts create mode 100644 build/index.js create mode 100644 build/types/adapter.d.ts create mode 100644 build/types/adapter.js create mode 100644 build/types/api.d.ts create mode 100644 build/types/api.js create mode 100644 build/types/base.d.ts create mode 100644 build/types/base.js create mode 100644 build/types/common.d.ts create mode 100644 build/types/common.js create mode 100644 build/types/errorCodes.d.ts create mode 100644 build/types/errorCodes.js create mode 100644 build/types/index.d.ts create mode 100644 build/types/index.js diff --git a/.gitignore b/.gitignore index 8f0e8a9..fe3ba7b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,5 @@ *.code-workspace node_modules nbproject -build/ Thumbs.db diff --git a/build/ActionContext.d.ts b/build/ActionContext.d.ts new file mode 100644 index 0000000..7dd80d6 --- /dev/null +++ b/build/ActionContext.d.ts @@ -0,0 +1,8 @@ +import type { BackEndCommandJsonFormOptions, JsonFormData, JsonFormSchema, ProgressOptions } from '.'; +import type { ProgressDialog } from './ProgressDialog'; +export interface ActionContext { + showMessage(text: ioBroker.StringOrTranslated): Promise; + showConfirmation(text: ioBroker.StringOrTranslated): Promise; + showForm(schema: JsonFormSchema, options?: BackEndCommandJsonFormOptions): Promise; + openProgress(title: string, options?: ProgressOptions): Promise; +} diff --git a/build/ActionContext.js b/build/ActionContext.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/build/ActionContext.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/build/DeviceManagement.d.ts b/build/DeviceManagement.d.ts new file mode 100644 index 0000000..4241553 --- /dev/null +++ b/build/DeviceManagement.d.ts @@ -0,0 +1,56 @@ +import type { AdapterInstance } from '@iobroker/adapter-core'; +import type { ActionContext } from './ActionContext'; +import type { ProgressDialog } from './ProgressDialog'; +import { type ActionButton, type DeviceDetails, type DeviceId, type DeviceInfo, type DeviceStatus, type ErrorResponse, type InstanceDetails, type JsonFormData, type JsonFormSchema, type RetVal } from './types'; +import type { BackendToGuiCommand, DeviceRefreshResponse, InstanceRefreshResponse } from './types/base'; +import type { ProgressOptions } from './types/common'; +export type DeviceLoadContext = { + addDevice(device: DeviceInfo): void; + setTotalDevices(count: number): void; +}; +export declare abstract class DeviceManagement { + protected readonly adapter: TAdapter; + private instanceInfo?; + private devices?; + private readonly communicationStateId; + private readonly deviceLoadContexts; + private readonly messageContexts; + constructor(adapter: TAdapter, communicationStateId?: string | boolean); + private ensureCommunicationState; + protected sendCommandToGui(command: BackendToGuiCommand): Promise; + protected get log(): ioBroker.Log; + protected getInstanceInfo(): RetVal; + protected abstract loadDevices(context: DeviceLoadContext): RetVal; + protected getDeviceInfo(_deviceId: TId): RetVal>; + protected getDeviceStatus(_deviceId: TId): RetVal; + protected getDeviceDetails(id: TId): RetVal | null | { + error: string; + }>; + private handleInstanceAction; + private handleDeviceAction; + private handleDeviceControl; + private handleDeviceControlState; + private onMessage; + private handleMessage; + private sendReply; +} +export declare class MessageContext implements ActionContext { + private readonly adapter; + private hasOpenProgressDialog; + private lastMessage?; + private progressHandler?; + constructor(msg: ioBroker.Message, adapter: AdapterInstance); + showMessage(text: ioBroker.StringOrTranslated): Promise; + showConfirmation(text: ioBroker.StringOrTranslated): Promise; + showForm(schema: JsonFormSchema, options?: { + data?: JsonFormData; + title?: ioBroker.StringOrTranslated; + buttons?: (ActionButton | 'apply' | 'cancel' | 'close')[]; + }): Promise; + openProgress(title: ioBroker.StringOrTranslated, options?: ProgressOptions): Promise; + sendFinalResult(result: ErrorResponse | DeviceRefreshResponse<'api', TId> | InstanceRefreshResponse): void; + sendControlResult(deviceId: TId, controlId: string, result: ErrorResponse | ioBroker.State): void; + handleProgress(message: ioBroker.Message): void; + private checkPreconditions; + private send; +} diff --git a/build/DeviceManagement.js b/build/DeviceManagement.js new file mode 100644 index 0000000..ec6ff0f --- /dev/null +++ b/build/DeviceManagement.js @@ -0,0 +1,540 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.MessageContext = exports.DeviceManagement = void 0; +const types_1 = require("./types"); +class DeviceManagement { + constructor(adapter, communicationStateId) { + this.adapter = adapter; + this.deviceLoadContexts = new Map(); + this.messageContexts = new Map(); + adapter.on('message', this.onMessage.bind(this)); + if (communicationStateId === true) { + // use standard ID `info.deviceManager` + this.communicationStateId = 'info.deviceManager'; + } + else if (communicationStateId) { + this.communicationStateId = communicationStateId; + } + if (this.communicationStateId) { + this.ensureCommunicationState().catch(e => this.log().error(`Cannot initialize communication state: ${e}`)); + } + } + async ensureCommunicationState() { + let stateObj = await this.adapter.getObjectAsync(this.communicationStateId); + if (!stateObj) { + stateObj = { + _id: this.communicationStateId, + type: 'state', + common: { + expert: true, + name: 'Communication with GUI for device manager', + type: 'string', + role: 'state', + def: '', + read: true, + write: false, + }, + native: {}, + }; + await this.adapter.setObjectAsync(this.communicationStateId, stateObj); + } + } + async sendCommandToGui(command) { + if (this.communicationStateId) { + await this.adapter.setState(this.communicationStateId, JSON.stringify(command), true); + } + else { + throw new Error('Communication state not found'); + } + } + get log() { + return this.adapter.log; + } + getInstanceInfo() { + // Overload this method if your adapter does not use BackendToGui communication and States/Objects in DeviceInfo + return { apiVersion: 'v3', communicationStateId: this.communicationStateId || undefined }; + } + getDeviceInfo(_deviceId) { + throw new Error('Do not send "infoUpdate" or "delete" command without implementing getDeviceInfo method!'); + } + getDeviceStatus(_deviceId) { + throw new Error('Do not send "statusUpdate" command without implementing getDeviceStatus method!'); + } + getDeviceDetails(id) { + return { id, schema: {} }; + } + handleInstanceAction(actionId, context, options) { + var _a; + if (!this.instanceInfo) { + this.log.warn(`Instance action ${actionId} was called before getInstanceInfo()`); + return { + error: { + code: types_1.ErrorCodes.E_INSTANCE_ACTION_NOT_INITIALIZED, + message: `Instance action ${actionId} was called before getInstanceInfo()`, + }, + }; + } + const action = (_a = this.instanceInfo.actions) === null || _a === void 0 ? void 0 : _a.find(a => a.id === actionId); + if (!action) { + this.log.warn(`Instance action ${actionId} is unknown`); + return { + error: { + code: types_1.ErrorCodes.E_INSTANCE_ACTION_UNKNOWN, + message: `Instance action ${actionId} is unknown`, + }, + }; + } + if (!('handler' in action) || !action.handler) { + this.log.warn(`Instance action ${actionId} is disabled because it has no handler`); + return { + error: { + code: types_1.ErrorCodes.E_INSTANCE_ACTION_NO_HANDLER, + message: `Instance action ${actionId} is disabled because it has no handler`, + }, + }; + } + return action.handler(context, options); + } + handleDeviceAction(deviceId, actionId, context, options) { + var _a; + if (!this.devices) { + this.log.warn(`Device action ${actionId} was called before loadDevices()`); + return { + error: { + code: types_1.ErrorCodes.E_DEVICE_ACTION_NOT_INITIALIZED, + message: `Device action ${actionId} was called before loadDevices()`, + }, + }; + } + const jsonId = JSON.stringify(deviceId); + const device = this.devices.get(jsonId); + if (!device) { + this.log.warn(`Device action ${actionId} was called on unknown device: ${jsonId}`); + return { + error: { + code: types_1.ErrorCodes.E_DEVICE_ACTION_DEVICE_UNKNOWN, + message: `Device action ${actionId} was called on unknown device: ${jsonId}`, + }, + }; + } + const action = (_a = device.actions) === null || _a === void 0 ? void 0 : _a.find(a => a.id === actionId); + if (!action) { + this.log.warn(`Device action ${actionId} doesn't exist on device ${jsonId}`); + return { + error: { + code: types_1.ErrorCodes.E_DEVICE_ACTION_UNKNOWN, + message: `Device action ${actionId} doesn't exist on device ${jsonId}`, + }, + }; + } + if (!('handler' in action) || !action.handler) { + this.log.warn(`Device action ${actionId} on ${jsonId} is disabled because it has no handler`); + return { + error: { + code: types_1.ErrorCodes.E_DEVICE_ACTION_NO_HANDLER, + message: `Device action ${actionId} on ${jsonId} is disabled because it has no handler`, + }, + }; + } + return action.handler(deviceId, context, options); + } + handleDeviceControl(deviceId, controlId, newState, context) { + var _a; + if (!this.devices) { + this.log.warn(`Device control ${controlId} was called before loadDevices()`); + return { + error: { + code: types_1.ErrorCodes.E_DEVICE_CONTROL_NOT_INITIALIZED, + message: `Device control ${controlId} was called before loadDevices()`, + }, + }; + } + const jsonId = JSON.stringify(deviceId); + const device = this.devices.get(jsonId); + if (!device) { + this.log.warn(`Device control ${controlId} was called on unknown device: ${jsonId}`); + return { + error: { + code: types_1.ErrorCodes.E_DEVICE_CONTROL_DEVICE_UNKNOWN, + message: `Device control ${controlId} was called on unknown device: ${jsonId}`, + }, + }; + } + const control = (_a = device.controls) === null || _a === void 0 ? void 0 : _a.find(a => a.id === controlId); + if (!control) { + this.log.warn(`Device control ${controlId} doesn't exist on device ${jsonId}`); + return { + error: { + code: types_1.ErrorCodes.E_DEVICE_CONTROL_UNKNOWN, + message: `Device control ${controlId} doesn't exist on device ${jsonId}`, + }, + }; + } + if (!control.handler) { + this.log.warn(`Device control ${controlId} on ${jsonId} is disabled because it has no handler`); + return { + error: { + code: types_1.ErrorCodes.E_DEVICE_CONTROL_NO_HANDLER, + message: `Device control ${controlId} on ${jsonId} is disabled because it has no handler`, + }, + }; + } + return control.handler(deviceId, controlId, newState, context); + } + // request state of control + handleDeviceControlState(deviceId, controlId, context) { + var _a; + if (!this.devices) { + this.log.warn(`Device get state ${controlId} was called before loadDevices()`); + return { + error: { + code: types_1.ErrorCodes.E_DEVICE_GET_STATE_NOT_INITIALIZED, + message: `Device control ${controlId} was called before loadDevices()`, + }, + }; + } + const jsonId = JSON.stringify(deviceId); + const device = this.devices.get(jsonId); + if (!device) { + this.log.warn(`Device get state ${controlId} was called on unknown device: ${jsonId}`); + return { + error: { + code: types_1.ErrorCodes.E_DEVICE_GET_STATE_DEVICE_UNKNOWN, + message: `Device control ${controlId} was called on unknown device: ${jsonId}`, + }, + }; + } + const control = (_a = device.controls) === null || _a === void 0 ? void 0 : _a.find(a => a.id === controlId); + if (!control) { + this.log.warn(`Device get state ${controlId} doesn't exist on device ${jsonId}`); + return { + error: { + code: types_1.ErrorCodes.E_DEVICE_GET_STATE_UNKNOWN, + message: `Device control ${controlId} doesn't exist on device ${jsonId}`, + }, + }; + } + if (!control.getStateHandler) { + this.log.warn(`Device get state ${controlId} on ${jsonId} is disabled because it has no handler`); + return { + error: { + code: types_1.ErrorCodes.E_DEVICE_GET_STATE_NO_HANDLER, + message: `Device get state ${controlId} on ${jsonId} is disabled because it has no handler`, + }, + }; + } + return control.getStateHandler(deviceId, controlId, context); + } + onMessage(obj) { + if (!obj.command.startsWith('dm:')) { + return; + } + void this.handleMessage(obj).catch(this.log.error); + } + async handleMessage(msg) { + var _a; + this.log.debug(`DeviceManagement received: ${JSON.stringify(msg)}`); + switch (msg.command) { + case 'dm:instanceInfo': { + this.instanceInfo = await this.getInstanceInfo(); + this.sendReply(Object.assign(Object.assign({}, this.instanceInfo), { actions: convertActions(this.instanceInfo.actions) }), msg); + return; + } + case 'dm:loadDevices': { + const context = new DeviceLoadContextImpl(msg, this.adapter); + this.deviceLoadContexts.set(msg._id, context); + await this.loadDevices(context); + if (context.complete()) { + this.deviceLoadContexts.delete(msg._id); + } + this.devices = context.devices.reduce((map, value) => { + const jsonId = JSON.stringify(value.id); + if (map.has(jsonId)) { + throw new Error(`Device ID ${jsonId} is not unique`); + } + map.set(jsonId, value); + return map; + }, new Map()); + return; + } + case 'dm:deviceInfo': { + const deviceInfo = await this.getDeviceInfo(msg.message); + this.sendReply(Object.assign(Object.assign({}, deviceInfo), { actions: convertActions(deviceInfo.actions), controls: convertControls(deviceInfo.controls) }), msg); + return; + } + case 'dm:deviceStatus': { + const deviceStatus = await this.getDeviceStatus(msg.message); + this.sendReply(deviceStatus, msg); + return; + } + case 'dm:deviceDetails': { + const details = await this.getDeviceDetails(msg.message); + this.sendReply(details, msg); + return; + } + case 'dm:instanceAction': { + const action = msg.message; + const context = new MessageContext(msg, this.adapter); + this.messageContexts.set(msg._id, context); + const result = await this.handleInstanceAction(action.actionId, context, { value: action.value }); + this.messageContexts.delete(msg._id); + context.sendFinalResult(result); + return; + } + case 'dm:deviceAction': { + const action = msg.message; + const context = new MessageContext(msg, this.adapter); + this.messageContexts.set(msg._id, context); + const result = await this.handleDeviceAction(action.deviceId, action.actionId, context, { + value: action.value, + }); + this.messageContexts.delete(msg._id); + if ('update' in result) { + // special handling for update responses (we need to update our cache and convert actions/controls before sending to GUI) + const update = result.update; + (_a = this.devices) === null || _a === void 0 ? void 0 : _a.set(JSON.stringify(update.id), update); + context.sendFinalResult({ + update: Object.assign(Object.assign({}, update), { actions: convertActions(update.actions), controls: convertControls(update.controls) }), + }); + } + else { + context.sendFinalResult(result); + } + return; + } + case 'dm:deviceControl': { + const control = msg.message; + const context = new MessageContext(msg, this.adapter); + this.messageContexts.set(msg._id, context); + const result = await this.handleDeviceControl(control.deviceId, control.controlId, control.state, context); + this.messageContexts.delete(msg._id); + context.sendControlResult(control.deviceId, control.controlId, result); + return; + } + case 'dm:deviceControlState': { + const control = msg.message; + const context = new MessageContext(msg, this.adapter); + this.messageContexts.set(msg._id, context); + const result = await this.handleDeviceControlState(control.deviceId, control.controlId, context); + this.messageContexts.delete(msg._id); + context.sendControlResult(control.deviceId, control.controlId, result); + return; + } + case 'dm:deviceLoadProgress': { + const { origin } = msg.message; + const context = this.deviceLoadContexts.get(origin); + if (!context) { + this.log.warn(`Unknown message origin: ${origin}`); + this.sendReply({ error: 'Unknown load progress origin' }, msg); + return; + } + if (context.handleProgress(msg)) { + this.deviceLoadContexts.delete(origin); + } + return; + } + case 'dm:actionProgress': { + const { origin } = msg.message; + const context = this.messageContexts.get(origin); + if (!context) { + this.log.warn(`Unknown message origin: ${origin}`); + this.sendReply({ error: 'Unknown action origin' }, msg); + return; + } + context.handleProgress(msg); + return; + } + } + } + sendReply(reply, msg) { + this.adapter.sendTo(msg.from, msg.command, reply, msg.callback); + } +} +exports.DeviceManagement = DeviceManagement; +class DeviceLoadContextImpl { + constructor(msg, adapter) { + this.adapter = adapter; + this.minBatchSize = 8; + this.devices = []; + this.sendNext = []; + this.completed = false; + this.respondTo = msg; + this.id = msg._id; + } + addDevice(device) { + this.devices.push(device); + this.sendNext.push(Object.assign(Object.assign({}, device), { actions: convertActions(device.actions), controls: convertControls(device.controls) })); + this.flush(); + } + setTotalDevices(count) { + this.totalDevices = count; + this.flush(); + } + complete() { + this.completed = true; + return this.flush(); + } + handleProgress(message) { + this.respondTo = message; + return this.flush(); + } + flush() { + if (this.sendNext.length <= this.minBatchSize && !this.completed) { + return false; + } + if (!this.respondTo) { + return false; + } + const reply = { + add: this.sendNext, + total: this.totalDevices, + next: this.completed ? undefined : { origin: this.id }, + }; + this.sendNext = []; + const msg = this.respondTo; + this.respondTo = undefined; + this.adapter.sendTo(msg.from, msg.command, reply, msg.callback); + return this.completed; + } +} +class MessageContext { + constructor(msg, adapter) { + this.adapter = adapter; + this.hasOpenProgressDialog = false; + this.lastMessage = msg; + } + showMessage(text) { + this.checkPreconditions(); + const promise = new Promise(resolve => { + this.progressHandler = () => resolve(); + }); + this.send({ type: 'message', message: text }); + return promise; + } + showConfirmation(text) { + this.checkPreconditions(); + const promise = new Promise(resolve => { + this.progressHandler = msg => resolve(!!msg.confirm); + }); + this.send({ type: 'confirm', confirm: text }); + return promise; + } + showForm(schema, options) { + this.checkPreconditions(); + const promise = new Promise(resolve => { + this.progressHandler = msg => resolve(msg.data); + }); + this.send({ + type: 'form', + form: Object.assign({ schema }, options), + }); + return promise; + } + openProgress(title, options) { + this.checkPreconditions(); + this.hasOpenProgressDialog = true; + const dialog = { + update: (update) => { + const promise = new Promise(resolve => { + this.progressHandler = () => resolve(); + }); + this.send({ type: 'progress', progress: update }, true); + return promise; + }, + close: () => { + const promise = new Promise(resolve => { + this.progressHandler = () => { + this.hasOpenProgressDialog = false; + resolve(); + }; + }); + this.send({ type: 'progress', progress: { open: false } }); + return promise; + }, + }; + const promise = new Promise(resolve => { + this.progressHandler = () => resolve(dialog); + }); + this.send({ type: 'progress', progress: Object.assign(Object.assign({ title }, options), { open: true }) }, true); + return promise; + } + sendFinalResult(result) { + this.send({ type: 'result', result }); + } + sendControlResult(deviceId, controlId, result) { + if (typeof result === 'object' && 'error' in result) { + this.send({ + type: 'result', + result: { + error: result.error, + deviceId, + controlId, + }, + }); + } + else { + this.send({ + type: 'result', + result: { + state: result, + deviceId, + controlId, + }, + }); + } + } + handleProgress(message) { + const currentHandler = this.progressHandler; + if (currentHandler && typeof message.message !== 'string') { + this.lastMessage = message; + this.progressHandler = undefined; + currentHandler(message.message); + } + } + checkPreconditions() { + if (this.hasOpenProgressDialog) { + throw new Error("Can't show another dialog while a progress dialog is open. Please call 'close()' on the dialog before opening another dialog."); + } + } + send(message, doNotClose) { + if (!this.lastMessage) { + throw new Error("No outstanding message, can't send a new one"); + } + this.adapter.sendTo(this.lastMessage.from, this.lastMessage.command, Object.assign(Object.assign({}, message), { origin: this.lastMessage.message.origin || this.lastMessage._id }), this.lastMessage.callback); + if (!doNotClose) { + // "progress" is exception. It will be closed with "close" flag + this.lastMessage = undefined; + } + } +} +exports.MessageContext = MessageContext; +function convertActions(actions) { + if (!actions) { + return undefined; + } + // detect duplicate IDs + const ids = new Set(); + actions.forEach(a => { + if (ids.has(a.id)) { + throw new Error(`Action ID ${a.id} is used twice, this would lead to unexpected behavior`); + } + ids.add(a.id); + }); + // remove handler function to send it as JSON + return actions.map((a) => (Object.assign(Object.assign({}, a), { handler: undefined, disabled: !a.handler && !a.url }))); +} +function convertControls(controls) { + if (!controls) { + return undefined; + } + // detect duplicate IDs + const ids = new Set(); + controls.forEach(a => { + if (ids.has(a.id)) { + throw new Error(`Control ID ${a.id} is used twice, this would lead to unexpected behavior`); + } + ids.add(a.id); + }); + // remove handler function to send it as JSON + return controls.map((a) => (Object.assign(Object.assign({}, a), { handler: undefined, getStateHandler: undefined }))); +} diff --git a/build/ProgressDialog.d.ts b/build/ProgressDialog.d.ts new file mode 100644 index 0000000..b0d7e06 --- /dev/null +++ b/build/ProgressDialog.d.ts @@ -0,0 +1,5 @@ +import type { ProgressUpdate } from './types/common'; +export interface ProgressDialog { + update(update: ProgressUpdate): Promise; + close(): Promise; +} diff --git a/build/ProgressDialog.js b/build/ProgressDialog.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/build/ProgressDialog.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/build/index.d.ts b/build/index.d.ts new file mode 100644 index 0000000..9cde45e --- /dev/null +++ b/build/index.d.ts @@ -0,0 +1,3 @@ +export type * from './ActionContext'; +export * from './DeviceManagement'; +export * from './types'; diff --git a/build/index.js b/build/index.js new file mode 100644 index 0000000..12e28ac --- /dev/null +++ b/build/index.js @@ -0,0 +1,19 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +__exportStar(require("./DeviceManagement"), exports); +// don't export * from "./MessageContext" as it is private +__exportStar(require("./types"), exports); diff --git a/build/types/adapter.d.ts b/build/types/adapter.d.ts new file mode 100644 index 0000000..97325f0 --- /dev/null +++ b/build/types/adapter.d.ts @@ -0,0 +1,8 @@ +import type * as base from './base'; +import type { DeviceId } from './common'; +export type ActionBase = base.ActionBase<'adapter'>; +export type InstanceAction = base.InstanceAction<'adapter'>; +export type DeviceAction = base.DeviceAction<'adapter', TId>; +export type InstanceDetails = base.InstanceDetails<'adapter'>; +export type DeviceInfo = base.DeviceInfo<'adapter', TId>; +export type DeviceControl = base.DeviceControl<'adapter', TId>; diff --git a/build/types/adapter.js b/build/types/adapter.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/build/types/adapter.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/build/types/api.d.ts b/build/types/api.d.ts new file mode 100644 index 0000000..9c87ed1 --- /dev/null +++ b/build/types/api.d.ts @@ -0,0 +1,65 @@ +import type * as base from './base'; +import type { ActionButton, DeviceId, ErrorResponse, JsonFormSchema, ProgressUpdate } from './common'; +export type ActionBase = base.ActionBase<'api'>; +export type InstanceAction = base.InstanceAction<'api'>; +export type DeviceAction = base.DeviceAction<'api'>; +export type InstanceDetails = base.InstanceDetails<'api'>; +export type DeviceInfo = base.DeviceInfo<'api'>; +export type DeviceControl = base.DeviceControl<'api'>; +export type DeviceRefreshResponse = base.DeviceRefreshResponse<'api'>; +export type InstanceRefreshResponse = base.InstanceRefreshResponse; +export type DeviceLoadIncrement = { + add: DeviceInfo[]; + total?: number; + next?: { + origin: number; + }; +}; +export type DmResponseBase = { + origin: number; +}; +export type DmControlResponse = DmResponseBase & { + type: 'result'; + result: { + deviceId: DeviceId; + controlId: string; + } & (ErrorResponse | { + state: ioBroker.State; + }); +}; +export type DmActionResultResponse = DmResponseBase & { + type: 'result'; + result: ErrorResponse | DeviceRefreshResponse | InstanceRefreshResponse; +}; +export type DmActionMessageResponse = DmResponseBase & { + type: 'message'; + message: ioBroker.StringOrTranslated; +}; +export type DmActionConfirmResponse = DmResponseBase & { + type: 'confirm'; + confirm: ioBroker.StringOrTranslated; +}; +export interface CommunicationForm { + title?: ioBroker.StringOrTranslated | null | undefined; + label?: ioBroker.StringOrTranslated | null | undefined; + noTranslation?: boolean; + schema: JsonFormSchema; + data?: Record; + buttons?: (ActionButton | 'apply' | 'cancel' | 'close')[]; + maxWidth?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + /** Minimal width of the dialog */ + minWidth?: number; + /** Always allow the apply button. Even when nothing was changed */ + ignoreApplyDisabled?: boolean; +} +export type DmActionFormResponse = DmResponseBase & { + type: 'form'; + form: CommunicationForm; +}; +export type DmActionProgressResponse = DmResponseBase & { + type: 'progress'; + progress: ProgressUpdate & { + open?: boolean; + }; +}; +export type DmActionResponse = DmActionResultResponse | DmActionMessageResponse | DmActionConfirmResponse | DmActionFormResponse | DmActionProgressResponse; diff --git a/build/types/api.js b/build/types/api.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/build/types/api.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/build/types/base.d.ts b/build/types/base.d.ts new file mode 100644 index 0000000..754d6ed --- /dev/null +++ b/build/types/base.d.ts @@ -0,0 +1,178 @@ +import type { ActionContext, ConfigConnectionType, ErrorResponse, MessageContext, ValueOrObject, ValueOrState, ValueOrStateOrObject } from '..'; +import type { ApiVersion, DeviceId, DeviceStatus, RetVal } from './common'; +type ActionType = 'api' | 'adapter'; +export type Color = 'primary' | 'secondary' | (string & {}); +export type ControlState = string | number | boolean | null; +/** Reserved action names */ +export declare const ACTIONS: { + /** This action will be called when user clicks on connection icon */ + STATUS: string; + /** This action will be called when the user clicks on enabled/disabled icon. The enabled/disabled icon will be shown only if the node status has "enabled" flag set to false or true */ + ENABLE_DISABLE: string; +}; +export interface ActionBase { + /** Unique (for this adapter) action ID. It could be the name from ACTIONS too, but in this case some predefined appearance will be applied */ + id: string; + /** + * This can either be base64 or the URL to an icon. + */ + icon?: 'edit' | 'rename' | 'delete' | 'refresh' | 'newDevice' | 'new' | 'add' | 'discover' | 'search' | 'unpairDevice' | 'pairDevice' | 'identify' | 'play' | 'stop' | 'pause' | 'forward' | 'next' | 'rewind' | 'previous' | 'lamp' | 'light' | 'backlight' | 'dimmer' | 'socket' | 'settings' | 'users' | 'group' | 'user' | 'info' | (string & {}); + description?: ioBroker.StringOrTranslated; + disabled?: T extends 'api' ? boolean : never; + color?: Color; + backgroundColor?: Color; + /** If true, the user will be asked for confirmation before executing the action */ + confirmation?: boolean | ioBroker.StringOrTranslated; + /** If defined, before the action is triggered, the non-empty text or number or checkbox will be asked */ + inputBefore?: { + /** This label will be shown for the text input */ + label: ioBroker.StringOrTranslated; + /** This type of input will be shown. Default is type */ + type?: 'text' | 'number' | 'checkbox' | 'select' | 'slider' | 'color'; + /** If a type is "select", the options must be defined */ + options?: { + label: ioBroker.StringOrTranslated; + value: string; + }[]; + /** Default value for the input */ + defaultValue?: string | number | boolean; + /** If true, the input could be empty */ + allowEmptyValue?: boolean; + /** Minimum value for the input (number or slider) */ + min?: number; + /** Maximum value for the input (number or slider) */ + max?: number; + /** Step value for the input (number or slider) */ + step?: number; + }; + /** Timeout in ms for waiting an answer from backend */ + timeout?: number; +} +export interface ChannelInfo { + name: ioBroker.StringOrTranslated; + description?: ioBroker.StringOrTranslated; + icon?: string; + color?: Color; + backgroundColor?: Color; + order?: number; +} +export interface ControlBase { + id: string; + type: 'button' | 'switch' | 'slider' | 'select' | 'icon' | 'color' | 'text' | 'number' | 'info'; + state?: ioBroker.State; + stateId?: string; + icon?: string; + iconOn?: string; + min?: number; + max?: number; + step?: number; + unit?: string; + label?: ioBroker.StringOrTranslated; + labelOn?: ioBroker.StringOrTranslated; + description?: ioBroker.StringOrTranslated; + color?: Color; + colorOn?: Color; + controlDelay?: number; + options?: { + label: ioBroker.StringOrTranslated; + value: ControlState; + icon?: string; + color?: Color; + }[]; + channel?: ChannelInfo; +} +export interface DeviceControl extends ControlBase { + handler?: TType extends 'api' ? never : (deviceId: TId, actionId: string, state: ControlState, context: MessageContext) => RetVal; + getStateHandler?: TType extends 'api' ? never : (deviceId: TId, actionId: string, context: MessageContext) => RetVal; +} +export type InstanceRefreshResponse = { + refresh: boolean; +}; +export type WithHandlerOrUrl = { + handler?: TType extends 'api' ? never : THandler; +} | { + url: ioBroker.StringOrTranslated; +}; +export type InstanceAction = ActionBase & WithHandlerOrUrl) => RetVal> & { + title: ioBroker.StringOrTranslated; +}; +export type DeviceUpdate = { + update: DeviceInfo; +}; +export type DeviceDelete = { + delete: TId; +}; +export type DeviceRefresh = 'all' | 'devices' | 'instance' | 'none'; +export type DeviceRefreshResponse = { + refresh: DeviceRefresh; +} | DeviceUpdate | DeviceDelete; +export type DeviceAction = ActionBase & WithHandlerOrUrl) => RetVal>>; +export interface InstanceDetails { + /** API Version: 1 - till 2025 (including), 2 - from 2026 */ + apiVersion: ApiVersion; + actions?: InstanceAction[]; + /** ID of state used for communication with GUI */ + communicationStateId?: string; + /** Human-readable label next to the identifier */ + identifierLabel?: ioBroker.StringOrTranslated; +} +export interface DeviceInfo { + /** ID of the device. Must be unique only in one adapter. Other adapters could have same IDs */ + id: TId; + /** Human-readable identifier of the device */ + identifier?: ValueOrObject; + /** Name of the device. It will be shown in the card header */ + name: ValueOrObject; + /** base64 or url icon for device card */ + icon?: ValueOrState; + manufacturer?: ValueOrStateOrObject; + model?: ValueOrStateOrObject; + /** Color or 'primary', 'secondary' for the text in the card header */ + color?: ValueOrState; + /** Background color of card header (you can use primary, secondary or color rgb value or hex) */ + backgroundColor?: ValueOrState; + status?: DeviceStatus | DeviceStatus[]; + /** Connection type, how the device is connected */ + connectionType?: ValueOrStateOrObject; + /** If this flag is true or false, the according indication will be shown. Additionally, if ACTIONS.ENABLE_DISABLE is implemented, this action will be sent to backend by clicking on this indication */ + enabled?: ValueOrState; + /** List of actions on the card */ + actions?: DeviceAction[]; + /** List of controls on the card. The difference of controls and actions is that the controls can show status (e.g. on/off) and can work directly with states */ + controls?: DeviceControl[]; + /** If true, the button `more` will be shown on the card and called `dm:deviceDetails` action to get the details */ + hasDetails?: ValueOrStateOrObject; + /** Device type for grouping */ + group?: { + key: string; + name?: ioBroker.StringOrTranslated; + icon?: string; + }; +} +export interface BackendToGuiCommandDeviceInfoUpdate { + /** Used for updating and for adding new device */ + command: 'infoUpdate'; + /** Device ID */ + deviceId: TId; + /** Backend can send directly new information about device to avoid extra request from GUI */ + info?: DeviceInfo; +} +export interface BackendToGuiCommandDeviceStatusUpdate { + /** Status of device was updated */ + command: 'statusUpdate'; + /** Device ID */ + deviceId: TId; + /** Backend can send directly new status to avoid extra request from GUI */ + status?: DeviceStatus; +} +export interface BackendToGuiCommandDeviceDelete { + /** Device was deleted */ + command: 'delete'; + deviceId: TId; +} +export interface BackendToGuiCommandAllUpdate { + /** Read ALL information about all devices anew */ + command: 'all'; +} +export type BackendToGuiCommand = BackendToGuiCommandDeviceInfoUpdate | BackendToGuiCommandDeviceStatusUpdate | BackendToGuiCommandDeviceDelete | BackendToGuiCommandAllUpdate; +export {}; diff --git a/build/types/base.js b/build/types/base.js new file mode 100644 index 0000000..113503a --- /dev/null +++ b/build/types/base.js @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ACTIONS = void 0; +/** Reserved action names */ +exports.ACTIONS = { + /** This action will be called when user clicks on connection icon */ + STATUS: 'status', + /** This action will be called when the user clicks on enabled/disabled icon. The enabled/disabled icon will be shown only if the node status has "enabled" flag set to false or true */ + ENABLE_DISABLE: 'enable/disable', +}; diff --git a/build/types/common.d.ts b/build/types/common.d.ts new file mode 100644 index 0000000..63114b4 --- /dev/null +++ b/build/types/common.d.ts @@ -0,0 +1,951 @@ +export type ApiVersion = 'v3'; +export type ConfigConnectionType = 'lan' | 'wifi' | 'bluetooth' | 'thread' | 'z-wave' | 'zigbee' | 'other'; +export interface ComplexDeviceId { + [key: string | number]: string | number | ComplexDeviceId; +} +export type DeviceId = string | number | ComplexDeviceId; +export type ValueOrObject = T | { + objectId: string; + property: string; +}; +export type ValueOrState = T | { + stateId: string; + mapping?: Record; +}; +export type ValueOrStateOrObject = T | ValueOrObject | ValueOrState; +export type DeviceStatus = 'connected' | 'disconnected' | { + battery?: ValueOrState; + connection?: ValueOrState<'connected' | 'disconnected'>; + rssi?: ValueOrState; + warning?: ValueOrState; +}; +export type ErrorResponse = { + error: { + code: number; + message: string; + }; +}; +export type RetVal = T | Promise; +export interface ProgressOptions { + indeterminate?: boolean; + value?: number; + label?: ioBroker.StringOrTranslated; +} +export interface ProgressUpdate extends ProgressOptions { + title?: ioBroker.StringOrTranslated; +} +type CustomCSSProperties = Record; +interface ObjectBrowserCustomFilter { + type?: ioBroker.ObjectType | ioBroker.ObjectType[]; + common?: { + type?: ioBroker.CommonType | ioBroker.CommonType[]; + role?: string | string[]; + custom?: '_' | '_dataSources' | true | (string & {}) | string[]; + }; +} +export type ObjectBrowserType = 'state' | 'instance' | 'channel' | 'device' | 'chart'; +export type ConfigItemType = 'accordion' | 'alive' | 'autocomplete' | 'autocompleteSendTo' | 'certCollection' | 'certificate' | 'certificates' | 'checkDocker' | 'checkLicense' | 'checkbox' | 'chips' | 'color' | 'coordinates' | 'cron' | 'custom' | 'datePicker' | 'deviceManager' | 'divider' | 'file' | 'fileSelector' | 'func' | 'header' | 'iframe' | 'iframeSendTo' | 'image' | 'imageSendTo' | 'infoBox' | 'instance' | 'interface' | 'ip' | 'jsonEditor' | 'language' | 'license' | 'number' | 'oauth2' | 'objectId' | 'panel' | 'password' | 'pattern' | 'port' | 'qrCode' | 'room' | 'select' | 'selectSendTo' | 'sendto' | 'setState' | 'slider' | 'state' | 'staticImage' | 'staticInfo' | 'staticLink' | 'staticText' | 'table' | 'tabs' | 'text' | 'textSendTo' | 'timePicker' | 'topic' | 'user' | 'uuid' | 'yamlEditor'; +export type ConfigIconType = 'add' | 'backlight' | 'book' | 'delete' | 'dimmer' | 'edit' | 'error' | 'group' | 'help' | 'identify' | 'info' | 'light' | 'lines' | 'next' | 'pair' | 'pause' | 'play' | 'previous' | 'qrcode' | 'refresh' | 'search' | 'send' | 'settings' | 'socket' | 'stop' | 'unpair' | 'upload' | 'user' | 'warning' | 'web' | string; +export interface ConfigItemConfirmData { + condition: string; + text?: ioBroker.StringOrTranslated; + title?: ioBroker.StringOrTranslated; + ok?: ioBroker.StringOrTranslated; + cancel?: ioBroker.StringOrTranslated; + type?: 'info' | 'warning' | 'error' | 'none'; + alsoDependsOn?: string[]; +} +export interface ConfigItem { + /** Type of the JSON config item */ + type: ConfigItemType; + /** Width of the control on "extra small" displays */ + xs?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; + /** Width of the control on "small" displays */ + sm?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; + /** Width of the control on "medium" displays */ + md?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; + /** Width of the control on "large" displays */ + lg?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; + /** Width of the control on "extra large" displays */ + xl?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; + /** If the control should be shown in a new line */ + newLine?: boolean; + /** Label of the control */ + label?: ioBroker.StringOrTranslated; + /** @deprecated use label */ + text?: ioBroker.StringOrTranslated; + /** Formula or false to hide the control: "data.attr === 5" */ + hidden?: string | boolean; + /** If true and the control is hidden, the place of the control will be still reserved for it */ + hideOnlyControl?: boolean; + /** JS function to calculate if the control is disabled. You can write "true" too */ + disabled?: string | boolean; + /** Help text of the control */ + help?: ioBroker.StringOrTranslated; + /** Link that will be opened by clicking on the help text */ + helpLink?: string; + style?: CustomCSSProperties; + darkStyle?: CustomCSSProperties; + validator?: string; + validatorErrorText?: string; + validatorNoSaveOnError?: boolean; + tooltip?: ioBroker.StringOrTranslated; + default?: boolean | number | string; + defaultFunc?: string; + defaultSendTo?: string; + /** Allow saving of configuration even with an error */ + allowSaveWithError?: boolean; + data?: string | number | boolean; + jsonData?: string; + button?: ioBroker.StringOrTranslated; + buttonTooltip?: ioBroker.StringOrTranslated; + buttonTooltipNoTranslation?: boolean; + placeholder?: ioBroker.StringOrTranslated; + noTranslation?: boolean; + onChange?: { + alsoDependsOn: string[]; + calculateFunc: string; + ignoreOwnChanges?: boolean; + }; + doNotSave?: boolean; + /** If the control should be shown ONLY in the expert mode */ + expertMode?: boolean; + noMultiEdit?: boolean; + confirm?: ConfigItemConfirmData; + icon?: ConfigIconType; + width?: string | number; + confirmDependsOn?: ConfigItemIndexed[]; + onChangeDependsOn?: ConfigItemIndexed[]; + hiddenDependsOn?: ConfigItemIndexed[]; + labelDependsOn?: ConfigItemIndexed[]; + helpDependsOn?: ConfigItemIndexed[]; +} +interface ConfigItemIndexed extends ConfigItem { + attr?: string; +} +interface ConfigItemTableIndexed extends ConfigItem { + attr?: string; + /** show filter options in the header of the table */ + filter?: boolean; + /** show sorting options in the header of the table */ + sort?: boolean; + /** tooltip in the header of the table */ + title?: string; +} +export interface ConfigItemAlive extends ConfigItem { + type: 'alive'; + /** check if the instance is alive. If not defined, it will be used current instance. You can use the ` $ {data.number} ` pattern in the text. */ + instance?: string; + /** the default text is `Instance %s is alive`, where %s will be replaced by `ADAPTER.0`. The translation must exist in i18n files. */ + textAlive?: string; + /** the default text is `Instance %s is not alive`, where %s will be replaced by `ADAPTER.0`. The translation must exist in i18n files. */ + textNotAlive?: string; +} +export interface ConfigItemSelectOption { + /** Label of option */ + label: ioBroker.StringOrTranslated; + /** Value of option */ + value: number | string; + /** Color of value */ + color?: string; + /** Formula or boolean value to show or hide the option */ + hidden?: string | boolean; +} +export interface ConfigItemPanel extends ConfigItem { + type: 'panel' | never; + /** Label of tab */ + label?: ioBroker.StringOrTranslated; + items: Record; + /** only possible as not part of tabs */ + collapsable?: boolean; + /** color of collapsable header `primary` or `secondary` or nothing */ + color?: 'primary' | 'secondary'; + /** CSS Styles in React format (`marginLeft` and not `margin-left`) for the Panel component */ + innerStyle?: CustomCSSProperties; + /** i18n definitions: true - load from a file, string - name of subdirectory, object - translations */ + i18n?: boolean | string | Record>; + command?: string; +} +export interface ConfigItemPattern extends ConfigItem { + type: 'pattern'; + /** if true - show copy button */ + copyToClipboard?: boolean; + /** pattern like 'https://${data.ip}:${data.port}' */ + pattern: string; +} +export interface ConfigItemChip extends ConfigItem { + type: 'chips'; + /** if it is defined, so the option will be stored as string with delimiter instead of an array. E.g., by `delimiter=;` you will get `a;b;c` instead of `['a', 'b', 'c']` */ + delimiter?: string; +} +export interface ConfigItemTabs extends ConfigItem { + type: 'tabs'; + /** Object with panels `{"tab1": {}, "tab2": {}...}` */ + items: Record; + /** `bottom`, `end`, `start` or `top`. Only for panels that has `icon` attribute. Default: `start` */ + iconPosition?: 'bottom' | 'end' | 'start' | 'top'; + /** CSS Styles in React format (`marginLeft` and not `margin-left`) for the Mui-Tabs component */ + tabsStyle?: CustomCSSProperties; + /** i18n definitions: true - load from a file, string - name of subdirectory, object - translations */ + i18n?: boolean | string | Record>; + command?: string; +} +export interface ConfigItemText extends ConfigItem { + type: 'text'; + /** max length of the text in the field */ + maxLength?: number; + /** @deprecated use maxLength */ + max?: number; + /** read-only field */ + readOnly?: boolean; + /** show copy-to-clipboard button, but only if disabled or read-only */ + copyToClipboard?: boolean; + /** default is true. Set this attribute to `false` if trim is not desired. */ + trim?: boolean; + /** the default is 1. Set this attribute to `2` or more if you want to have a textarea with more than one row. */ + minRows?: number; + /** max rows of textarea. Used only if `minRows` > 1. */ + maxRows?: number; + /** if true, the clear button will not be shown */ + noClearButton?: boolean; + /** if true, the text will be validated as JSON */ + validateJson?: boolean; + /** if true, the JSON will be validated only if the value is not empty */ + allowEmpty?: boolean; + /** the value is time in ms or a string. Used only with readOnly flag */ + time?: boolean; +} +export interface ConfigItemColor extends ConfigItem { + type: 'color'; + /** if true, the clear button will not be shown */ + noClearButton?: boolean; +} +export interface ConfigItemCheckbox extends ConfigItem { + type: 'checkbox'; + /** Same as disabled */ + readOnly?: boolean; +} +export interface ConfigItemNumber extends ConfigItem { + type: 'number'; + min?: number; + max?: number; + step?: number; + readOnly?: boolean; + /** Unit */ + unit?: string; +} +export interface ConfigItemOAuth2 extends ConfigItem { + type: 'oauth2'; + saveTokenIn?: string; + identifier: 'spotify' | 'google' | 'dropbox' | 'microsoft' | string; + scope?: string; + refreshLabel?: ioBroker.StringOrTranslated; +} +export interface ConfigItemQrCode extends ConfigItem { + type: 'qrCode'; + /** Data to show in the QR code */ + data: string; + /** Size of the QR code */ + size?: number; + /** Foreground color */ + fgColor?: string; + /** Background color */ + bgColor?: string; + /** QR code level */ + level?: 'L' | 'M' | 'Q' | 'H'; +} +export interface ConfigItemPassword extends ConfigItem { + type: 'password'; + /** repeat password must be compared with password */ + repeat?: boolean; + /** true if allow viewing the password by toggling the view button (only for a new password while entering) */ + visible?: boolean; + /** The read-only flag. Visible is automatically true if readOnly is true */ + readOnly?: boolean; + /** max length of the text in the field */ + maxLength?: number; + /** @deprecated use maxLength */ + max?: number; +} +export interface ConfigItemObjectId extends ConfigItem { + type: 'objectId'; + /** Desired type: `channel`, `device`, ... (has only `state` by default). It is plural, because `type` is already occupied. */ + types?: ObjectBrowserType | ObjectBrowserType[]; + /** Show only this root object and its children */ + root?: string; + /** + * Cannot be used together with `type` settings. It is an object and not a JSON string. Examples + * - `{common: {custom: true}}` - show only objects with some custom settings + * - `{common: {custom: 'sql.0'}}` - show only objects with sql.0 custom settings (only of the specific instance) + * - `{common: {custom: '_dataSources'}}` - show only objects of adapters `influxdb` or `sql` or `history` + * - `{common: {custom: 'adapterName.'}}` - show only objects of custom settings of specific adapter (all instances) + * - `{type: 'channel'}` - show only channels + * - `{type: ['channel', 'device']}` - show only channels and devices + * - `{common: {type: 'number'}` - show only states of type 'number + * - `{common: {type: ['number', 'string']}` - show only states of type 'number and string + * - `{common: {role: 'switch'}` - show only states with roles starting from switch + * - `{common: {role: ['switch', 'button']}` - show only states with roles starting from `switch` and `button` + */ + customFilter?: ObjectBrowserCustomFilter; + /** some predefined search filters */ + filters?: { + id?: string; + name?: string; + room?: string[]; + func?: string[]; + role?: string[]; + type?: string[]; + custom?: string[]; + }; + /** Cannot be used together with `type` settings. It is a function that will be called for every object and must return true or false. Example: `obj.common.type === 'number'` */ + filterFunc?: (obj: ioBroker.Object) => boolean; +} +export interface ConfigItemSlider extends ConfigItem { + type: 'slider'; + min?: number; + max?: number; + step?: number; + /** Unit of slider */ + unit?: string; +} +export interface ConfigItemTopic extends ConfigItem { + type: 'topic'; + /** max length of the text in the field */ + maxLength?: number; + /** @deprecated use maxLength */ + max?: number; +} +export interface ConfigItemIP extends ConfigItem { + type: 'ip'; + listenOnAllPorts?: boolean; + onlyIp4?: boolean; + onlyIp6?: boolean; + noInternal?: boolean; +} +export interface ConfigItemUser extends ConfigItem { + type: 'user'; + /** without "system.user." */ + short?: boolean; +} +export interface ConfigItemStaticDivider extends ConfigItem { + type: 'divider'; + color?: 'primary' | 'secondary' | string; + height?: string | number; +} +export interface ConfigItemStaticHeader extends ConfigItem { + type: 'header'; + size?: 1 | 2 | 3 | 4 | 5; + text: ioBroker.StringOrTranslated; + noTranslation?: boolean; +} +export interface ConfigItemStaticImage extends ConfigItem { + type: 'staticImage'; + /** name of picture (from admin directory) */ + src: string; + /** optional HTTP link */ + href?: string; +} +export interface ConfigItemStaticText extends Omit { + type: 'staticText'; + /** multi-language text */ + text: ioBroker.StringOrTranslated; + /** @deprecated use text */ + label?: ioBroker.StringOrTranslated; + /** link. Link could be dynamic like `#tab-objects/customs/${data.parentId} */ + href?: string; + /** target of the link: _self, _blank or window name. For relative links the default is _self and for absolute - _blank */ + target?: string; + /** If the GUI should be closed after a link was opened (only if the target is equal to '_self') */ + close?: boolean; + /** show a link as a button */ + button?: boolean; + /** type of button (`outlined`, `contained`, `text`) */ + variant?: 'contained' | 'outlined' | 'text'; + /** color of button (e.g. `primary`) */ + color?: 'primary' | 'secondary' | 'grey'; + /** if icon should be shown: `auth`, `send`, `web`, `warning`, `error`, `info`, `search`, `book`, `help`, `upload`. You can use `base64` icons (it starts with `data:image/svg+xml;base64,...`) or `jpg/png` images (ends with `.png`) . (Request via issue if you need more icons) */ + icon?: ConfigIconType; + /** styles for the button */ + controlStyle?: CustomCSSProperties; +} +export interface ConfigItemStaticInfo extends Omit { + type: 'staticInfo'; + /** multi-language text or value */ + data: ioBroker.StringOrTranslated | number | boolean; + /** Base64 icon */ + labelIcon?: string; + /** Unit */ + unit?: ioBroker.StringOrTranslated; + /** Normally, the title and value are shown on the left and right of the line. With this flag, the value will appear just after the label*/ + narrow?: boolean; + /** Add to the label the colon at the end if not exist in the label */ + addColon?: boolean; + /** Value should blink when updated (true or color) */ + blinkOnUpdate?: boolean | string; + /** Value should blink continuously (true or color) */ + blink?: boolean | string; + /** Show a copy-to-clipboard button for value */ + copyToClipboard?: boolean; + /** Label style */ + styleLabel?: CustomCSSProperties; + /** Value style */ + styleValue?: CustomCSSProperties; + /** Unit style */ + styleUnit?: CustomCSSProperties; + /** Font size */ + size?: number | 'small' | 'normal' | 'large'; + /** Highlight line on mouse over */ + highlight?: boolean; + /** Show boolean values as a checkbox */ + booleanAsCheckbox?: boolean; + /** Show string values as HTML */ + html?: boolean; +} +export interface ConfigItemInfoBox extends ConfigItem { + type: 'infoBox'; + /** multi-language text */ + text: ioBroker.StringOrTranslated; + /** multi-language title */ + title?: ioBroker.StringOrTranslated; + /** The type determines the color and symbol */ + boxType?: 'warning' | 'info' | 'error' | 'ok'; + /** If the box is closeable */ + closeable?: boolean; + /** Use together with `closeable: true`. If the box is closed or not. In this case, it will be controlled from outside */ + closed?: boolean; + /** Icon position */ + iconPosition?: 'top' | 'middle'; +} +export interface ConfigItemRoom extends ConfigItem { + type: 'room'; + short?: boolean; + allowDeactivate?: boolean; +} +export interface ConfigItemFunc extends ConfigItem { + type: 'func'; + short?: boolean; + allowDeactivate?: boolean; +} +export interface ConfigItemSelect extends ConfigItem { + type: 'select'; + /** + * `[{label: {en: "option 1"}, value: 1}, ...]` or + * `[{"items": [{"label": "Val1", "value": 1}, {"label": "Val2", value: "2}], "name": "group1"}, {"items": [{"label": "Val3", "value": 3}, {"label": "Val4", value: "4}], "name": "group2"}, {"label": "Val5", "value": 5}]` + */ + options: (ConfigItemSelectOption | { + items: ConfigItemSelectOption[]; + label: ioBroker.StringOrTranslated; + value?: number | string; + color?: string; + hidden?: string | boolean; + })[]; + attr?: string; + /** If multiple selection is possible. In this case, the value will be an array */ + multiple?: boolean; + /** show an item even if no label was found for it (by multiple), default=`true` */ + showAllValues?: boolean; +} +export interface ConfigItemAutocomplete extends ConfigItem { + type: 'autocomplete'; + options: (string | ConfigItemSelectOption)[]; + freeSolo?: boolean; +} +export interface ConfigItemSetState extends ConfigItem { + type: 'setState'; + /** `system.adapter.myAdapter.%INSTANCE%.test`, you can use the placeholder `%INSTANCE%` to replace it with the current instance name */ + id: string; + /** false (default false) */ + ack?: boolean; + /** '${data.myText}_test' or number. Type will be detected automatically from the state type and converting done too */ + val: ioBroker.StateValue; + /** Alert, which will be shown by pressing the button */ + okText?: ioBroker.StringOrTranslated; + variant?: 'contained' | 'outlined'; + color?: 'primary' | 'secondary' | 'grey'; + /** Error translations */ + error?: { + [error: string]: ioBroker.StringOrTranslated; + }; +} +export interface ConfigItemAutocompleteSendTo extends Omit { + type: 'autocompleteSendTo'; + command?: string; + jsonData?: string; + options?: (string | ConfigItemSelectOption)[]; + data?: Record; + freeSolo?: boolean; + /** max length of the text in the field */ + maxLength?: number; + /** @deprecated use maxLength */ + max?: string; + alsoDependsOn?: string[]; +} +export interface ConfigItemAccordion extends ConfigItem { + type: 'accordion'; + /** Title shown on the accordion */ + titleAttr?: string; + /** If delete or add disabled, If noDelete is false, add, delete and move up/down should work */ + noDelete?: boolean; + /** If the clone button should be shown. If true, the clone button will be shown. If attribute name, this name will be unique. */ + clone?: boolean | string; + /** Items of accordion */ + items: ConfigItemIndexed[]; +} +export interface ConfigItemDivider extends ConfigItem { + type: 'divider'; + color?: 'primary' | 'secondary' | string; + height?: string | number; +} +export interface ConfigItemHeader extends ConfigItem { + type: 'header'; + text?: ioBroker.StringOrTranslated; + size?: 1 | 2 | 3 | 4 | 5 | 6; +} +export interface ConfigItemCoordinates extends ConfigItem { + type: 'coordinates'; + /** divider between latitude and longitude. Default "," (Used if longitudeName and latitudeName are not defined) */ + divider?: string; + /** init field with current coordinates if empty */ + autoInit?: boolean; + /** if defined, the longitude will be stored in this attribute, divider will be ignored */ + longitudeName?: string; + /** if defined, the latitude will be stored in this attribute, divider will be ignored */ + latitudeName?: string; + /** if defined, the checkbox with "Use system settings" will be shown and latitude, longitude will be read from system.config, a boolean will be saved to the given name */ + useSystemName?: string; + /** max length of the text in the field */ + maxLength?: number; + /** @deprecated use maxLength */ + max?: number; +} +export interface ConfigItemCustom extends ConfigItem { + type: 'custom'; + /** location of Widget, like "custom/customComponents.js" */ + url: string; + /** New format for components written in TypeScript */ + bundlerType?: 'module'; + /** Component name, like "ConfigCustomBackItUpSet/Components/AdapterExist" */ + name: string; + /** i18n */ + i18n: boolean | Record; + /** custom properties */ + custom?: { + [prop: string]: any; + }; +} +export interface ConfigItemDatePicker extends ConfigItem { + type: 'datePicker'; + /** max length of the text in the field */ + maxLength?: number; + /** @deprecated use maxLength */ + max?: number; +} +export interface ConfigItemDeviceManager extends ConfigItem { + type: 'deviceManager'; +} +export interface ConfigItemLanguage extends ConfigItem { + type: 'language'; + system?: boolean; + changeGuiLanguage?: boolean; +} +export interface ConfigItemPort extends ConfigItem { + type: 'port'; + min?: number; + max?: number; + readOnly?: boolean; +} +export interface ConfigItemIFrame extends ConfigItem { + type: 'iframe'; + url?: string; + sandbox?: string; + allowFullscreen?: boolean; + frameBorder?: number | string; + /** if true, the iframe will be loaded only when it becomes visible */ + lazyLoad?: 'lazy' | 'eager'; + /** if true, the iframe will be reloaded every time it becomes visible */ + reloadOnShow?: boolean; + /** CSS Styles in React format (`marginLeft` and not `margin-left`) for the IFrame component */ + innerStyle?: CustomCSSProperties; +} +export interface ConfigItemIFrameSendTo extends Omit { + type: 'iframeSendTo'; + command?: string; + alsoDependsOn?: string[]; + data?: Record; +} +export interface ConfigItemImageSendTo extends Omit { + type: 'imageSendTo'; + command?: string; + alsoDependsOn?: string[]; + height?: number | string; + data?: Record; +} +export interface ConfigItemSendTo extends Omit { + type: 'sendto'; + command?: string; + jsonData?: string; + data?: Record; + /** Translations for possible result codes. E.g. `{"RESULT_OK": "Operation successful"}`. The translation must exist in i18n files. */ + result?: Record; + /** Translations for possible error codes. E.g. `{"ERR_NO_OBJECT": "Object not found"}`. The translation must exist in i18n files. */ + error?: Record; + variant?: 'contained' | 'outlined'; + openUrl?: boolean; + reloadBrowser?: boolean; + window?: string; + icon?: ConfigIconType; + useNative?: boolean; + showProcess?: boolean; + timeout?: number; + onLoaded?: boolean; + color?: 'primary' | 'secondary'; + /** button tooltip */ + title?: ioBroker.StringOrTranslated; + alsoDependsOn?: string[]; + container?: 'text' | 'div' | 'html'; + copyToClipboard?: boolean; + /** Styles for the button itself */ + controlStyle?: CustomCSSProperties; +} +export interface ConfigItemState extends ConfigItem { + type: 'state'; + /** Describes which object ID should be taken for the controlling. The ID is without `ADAPTER.I.` prefix */ + oid: string; + /** The `oid` is absolute and no need to add `ADAPTER.I` or `system.adapter.ADAPTER.I.` to oid */ + foreign?: boolean; + /** If true, the state will be taken from `system.adapter.ADAPTER.I` and not from `ADAPTER.I` */ + system?: boolean; + /** How the value of the state should be shown */ + control?: 'text' | 'html' | 'input' | 'slider' | 'select' | 'button' | 'switch' | 'number'; + /** If true, the state will be shown as switch, select, button, slider or text input. Used only if no control property is defined */ + controlled?: boolean; + /** Add a unit to the value */ + unit?: string; + /** this text will be shown if the value is true */ + trueText?: string; + /** Style of the text if the value is true */ + trueTextStyle?: CustomCSSProperties; + /** this text will be shown if the value is false or if the control is a "button" */ + falseText?: string; + /** Style of the text if the value is false or if the control is a "button" */ + falseTextStyle?: CustomCSSProperties; + /** This image will be shown if the value is true */ + trueImage?: string; + /** This image will be shown if the value is false or if the control is a "button" */ + falseImage?: string; + /** Minimum value for control type slider or number */ + min?: number; + /** Maximum value for control type slider or number */ + max?: number; + /** Step value for control type slider or number */ + step?: number; + /** delay in ms for slider or number */ + controlDelay?: number; + /** Variant of button */ + variant?: 'contained' | 'outlined' | 'text'; + /** Defines if the control is read-only. Applied only to 'input', 'slider', 'select', 'button', 'switch', 'number' */ + readOnly?: boolean; + /** Base64 icon */ + labelIcon?: string; + /** Normally, the title and value are shown on the left and right of the line. With this flag, the value will appear just after the label*/ + narrow?: boolean; + /** Add to the label the colon at the end if not exist in the label */ + addColon?: boolean; + /** Value should blink when updated (true or color) */ + blinkOnUpdate?: boolean | string; + /** Font size */ + size?: number | 'small' | 'normal' | 'large'; + /** Optional value that will be sent for the button */ + buttonValue?: ioBroker.StateValue; + /** Show the SET button. The value in this case will be sent only when the button is pressed. You can define the text of the button. The default text is "Set" */ + showEnterButton?: boolean | ioBroker.StringOrTranslated; + /** The value in this case will be sent only when the "Enter" button is pressed. It can be combined with `showEnterButton` */ + setOnEnterKey?: boolean; + /** Options for `select`. If not defiled, the `common.states` in the object must exist. */ + options?: (string | ConfigItemSelectOption)[]; +} +export interface ConfigItemTextSendTo extends Omit { + type: 'textSendTo'; + container?: 'text' | 'div'; + /** if true - show copy-to-clipboard button */ + copyToClipboard?: boolean; + /** by change of which attributes, the command must be resent */ + alsoDependsOn?: string[]; + /** sendTo command */ + command?: string; + /** string - `{"subject1": "${data.subject}", "options1": {"host": "${data.host}"}}`. This data will be sent to the backend */ + jsonData?: string; + /** object - `{"subject1": 1, "data": "static"}`. You can specify jsonData or data, but not both. This data will be sent to the backend if jsonData is not defined. */ + data?: Record; +} +export interface ConfigItemSelectSendTo extends Omit { + type: 'selectSendTo'; + /** allow manual editing. Without a drop-down menu (if the instance is offline). Default `true`. */ + manual?: boolean; + /** Multiple choice select */ + multiple?: boolean; + /** show an item even if no label was found for it (by multiple), default=`true` */ + showAllValues?: boolean; + /** if true, the clear button will not be shown */ + noClearButton?: boolean; + /** sendTo command */ + command?: string; + /** string - `{"subject1": "${data.subject}", "options1": {"host": "${data.host}"}}`. This data will be sent to the backend */ + jsonData?: string; + /** object - `{"subject1": 1, "data": "static"}`. You can specify jsonData or data, but not both. This data will be sent to the backend if jsonData is not defined. */ + data?: Record; + /** by change of which attributes, the command must be resent */ + alsoDependsOn?: string[]; +} +export interface ConfigItemTable extends ConfigItem { + type: 'table'; + items?: ConfigItemTableIndexed[]; + /** Define the name of the attribute of the item which should be shown as a title of the item in cards mode. */ + titleAttribute?: string; + /** If delete or add disabled, If noDelete is false, add, delete and move up/down should work */ + noDelete?: boolean; + /** @deprecated don't use */ + objKeyName?: string; + /** @deprecated don't use */ + objValueName?: string; + /** If add allowed even if a filter is set */ + allowAddByFilter?: boolean; + /** The number of lines from which the second adding button at the bottom of the table will be shown. Default 5 */ + showSecondAddAt?: number; + /** Show the first plus button on top of the first column and not on the left. */ + showFirstAddOnTop?: boolean; + /** If the clone button should be shown. If true, the clone button will be shown. If attribute name, this name will be unique. */ + clone?: boolean | string; + /** If export button should be shown. Export as a csv file. */ + export?: boolean; + /** If import button should be shown. Import from csv file. */ + import?: boolean; + /** Show table in compact mode */ + compact?: boolean; + /** Specify the 'attr' name of columns which need to be unique */ + uniqueColumns?: string[]; + /** These items will be encrypted before saving with a simple (not SHA) encryption method */ + encryptedAttributes?: string[]; + /** Breakpoint that will be rendered as cards */ + useCardFor?: ('xs' | 'sm' | 'md' | 'lg' | 'xl')[]; +} +export interface ConfigItemTimePicker extends ConfigItem { + type: 'timePicker'; + /** format passed to the date picker defaults to `HH:mm:ss` */ + format?: string; + views?: ('hours' | 'minutes' | 'seconds')[]; + /** Represent the available time steps for each view. Defaults to `{ hours: 1, minutes: 5, seconds: 5 }` */ + timeSteps?: { + hours?: number; + minutes?: number; + seconds?: number; + }; + /** @deprecated use timeSteps */ + timesteps?: { + hours?: number; + minutes?: number; + seconds?: number; + }; + /** `fullDate` or `HH:mm:ss`. Defaults to full date for backward compatibility reasons */ + returnFormat?: string; +} +export interface ConfigItemCertCollection extends ConfigItem { + type: 'certCollection'; + leCollectionName?: string; +} +export interface ConfigItemCRON extends ConfigItem { + type: 'cron'; + /** show CRON with "minutes", "seconds" and so on */ + complex?: boolean; + /** show simple CRON settings */ + simple?: boolean; +} +export interface ConfigItemCertificateSelect extends ConfigItem { + type: 'certificate'; +} +export interface ConfigItemLicense extends ConfigItem { + type: 'license'; + /** array of paragraphs with texts, which will be shown each as a separate paragraph */ + texts?: string[]; + /** URL to the license file (e.g. https://raw.githubusercontent.com/ioBroker/ioBroker.docs/master/LICENSE) */ + licenseUrl?: string; + /** Title of the license dialog */ + title?: string; + /** Text of the agreed button */ + agreeText?: string; + /** If defined, the checkbox with the given name will be shown. If checked, the agreed button will be enabled. */ + checkBox?: string; +} +export interface ConfigItemCertificates extends ConfigItem { + type: 'certificates'; + leCollectionName?: string; + certPublicName?: string; + certPrivateName?: string; + certChainedName?: string; +} +export interface ConfigItemCheckLicense extends ConfigItem { + type: 'checkLicense'; + /** Check UUID */ + uuid?: boolean; + /** Check version */ + version?: boolean; + variant?: 'text' | 'outlined' | 'contained'; + color?: 'primary' | 'secondary'; +} +export interface ConfigItemCheckDocker extends ConfigItem { + type: 'checkDocker'; + /** Hide the version of docker */ + hideVersion?: boolean; +} +export interface ConfigItemUUID extends ConfigItem { + type: 'uuid'; +} +export interface ConfigItemJsonEditor extends ConfigItem { + type: 'jsonEditor'; + /** if false, the text will be not validated as JSON */ + validateJson?: boolean; + /** if true, the JSON will be validated only if the value is not empty */ + allowEmpty?: boolean; + /** Allow JSON5 format. Default is disabled */ + json5?: boolean; + /** Do not allow saving the value if error in JSON or JSON5 */ + doNotApplyWithError?: boolean; + /** Open the editor in read-only mode - editor can be opened but content cannot be modified */ + readOnly?: boolean; +} +export interface ConfigItemYamlEditor extends ConfigItem { + type: 'yamlEditor'; + /** if false, the text will be not validated as YAML */ + validateYaml?: boolean; + /** if true, the YAML will be validated only if the value is not empty */ + allowEmpty?: boolean; + /** Do not allow saving the value if error in YAML */ + doNotApplyWithError?: boolean; + /** Open the editor in read-only mode - editor can be opened but content cannot be modified */ + readOnly?: boolean; +} +export interface ConfigItemInterface extends ConfigItem { + type: 'interface'; + /** do not show loopback interface (127.0.0.1) */ + ignoreLoopback?: boolean; + /** do not show internal interfaces (normally it is 127.0.0.1 too) */ + ignoreInternal?: boolean; +} +export interface ConfigItemImageUpload extends ConfigItem { + type: 'image'; + /** the name of a file is the structure name. In the below example `login-bg.png` is the file name for `writeFile("myAdapter.INSTANCE", "login-bg.png")` */ + filename?: string; + /** HTML accept attribute, like `{ 'image/**': [], 'application/pdf': ['.pdf'] }`, default `{ 'image/*': [] }` */ + accept?: Record; + /** maximal size of a file to upload */ + maxSize?: number; + /** if true, the image will be saved as data-url in attribute, elsewise as binary in file storage */ + base64?: boolean; + /** if true, allow the user to crop the image */ + crop?: boolean; +} +export interface ConfigItemInstanceSelect extends ConfigItem { + type: 'instance'; + /** name of adapter. With the special name `_dataSources` you can get all adapters with the flag `common.getHistory`. */ + adapter?: string; + /** optional list of adapters that should be shown. If not defined, all adapters will be shown. Only active if the ` adapter ` attribute is not defined. */ + adapters?: string[]; + /** if true. The additional option "deactivate" is shown */ + allowDeactivate?: boolean; + /** if true. Only enabled instances will be shown */ + onlyEnabled?: boolean; + /** value will look like `system.adapter.ADAPTER.0` and not `ADAPTER.0` */ + long?: boolean; + /** value will look like `0` and not `ADAPTER.0` */ + short?: boolean; + /** Add to the options "all" option with value `*` */ + all?: boolean; +} +export interface ConfigItemFile extends ConfigItem { + type: 'file'; + /** if a user can manually enter the file name and not only through a select dialog */ + disableEdit?: boolean; + /** limit selection to one specific object of type `meta` and the following path (not mandatory) */ + limitPath?: string; + /** like `['png', 'svg', 'bmp', 'jpg', 'jpeg', 'gif']` */ + filterFiles?: string[]; + /** allowed upload of files */ + allowUpload?: boolean; + /** allowed download of files (default true) */ + allowDownload?: boolean; + /** allowed creation of folders */ + allowCreateFolder?: boolean; + /** allowed tile view (default true) */ + allowView?: boolean; + /** show toolbar (default true) */ + showToolbar?: boolean; + /** user can select only folders (e.g., for the uploading path) */ + selectOnlyFolders?: boolean; + /** trim the filename */ + trim?: boolean; + /** max length of the file name */ + maxLength?: number; + /** @deprecated use maxLength */ + max?: number; +} +export interface ConfigItemFileSelector extends ConfigItem { + type: 'fileSelector'; + /** File extension pattern. Allowed `**\/*.ext` to show all files from subfolders too, `*.ext` to show from the root folder or `folderName\/*.ext` to show all files in subfolder `folderName`. Default `**\/*.*`. */ + pattern: string; + /** type of files: `audio`, `image`, `text` */ + fileTypes?: 'audio' | 'image' | 'text'; + /** Object ID of the type `meta`. You can use special placeholder `%INSTANCE%`: like `myAdapter.%INSTANCE%.files` */ + objectID?: string; + /** path, where the uploaded files will be stored. Like `folderName`. If not defined, no upload field will be shown. To upload in the root, set this field to `/`. */ + upload?: string; + /** Show the refresh button near the select. */ + refresh?: boolean; + /** max file size (default 2MB) */ + maxSize?: number; + /** show folder name even if all files in the same folder */ + withFolder?: boolean; + /** Allow deletion of files */ + delete?: boolean; + /** Do not show `none` option */ + noNone?: boolean; + /** Do not show the size of files */ + noSize?: boolean; +} +export type ConfigItemAny = ConfigItemAlive | ConfigItemAutocomplete | ConfigItemAutocompleteSendTo | ConfigItemPanel | ConfigItemTabs | ConfigItemText | ConfigItemNumber | ConfigItemOAuth2 | ConfigItemColor | ConfigItemCheckbox | ConfigItemSlider | ConfigItemIP | ConfigItemInfoBox | ConfigItemUser | ConfigItemRoom | ConfigItemFunc | ConfigItemSelect | ConfigItemAccordion | ConfigItemCoordinates | ConfigItemDivider | ConfigItemHeader | ConfigItemCustom | ConfigItemDatePicker | ConfigItemDeviceManager | ConfigItemLanguage | ConfigItemPort | ConfigItemSendTo | ConfigItemState | ConfigItemTable | ConfigItemTimePicker | ConfigItemTextSendTo | ConfigItemSelectSendTo | ConfigItemCertCollection | ConfigItemCertificateSelect | ConfigItemCertificates | ConfigItemUUID | ConfigItemCheckDocker | ConfigItemCheckLicense | ConfigItemPattern | ConfigItemChip | ConfigItemCRON | ConfigItemFile | ConfigItemFileSelector | ConfigItemIFrame | ConfigItemIFrameSendTo | ConfigItemImageSendTo | ConfigItemInstanceSelect | ConfigItemImageUpload | ConfigItemInterface | ConfigItemJsonEditor | ConfigItemYamlEditor | ConfigItemLicense | ConfigItemPassword | ConfigItemSetState | ConfigItemStaticDivider | ConfigItemStaticHeader | ConfigItemStaticInfo | ConfigItemStaticImage | ConfigItemStaticText | ConfigItemTopic | ConfigItemObjectId | ConfigItemQrCode; +export type ActionButton = { + label: ioBroker.StringOrTranslated; + type: 'apply' | 'cancel' | 'copyToClipboard'; + noTranslation?: boolean; + /** base64 or icon name */ + icon?: ConfigIconType; + variant?: 'contained' | 'outlined' | 'text'; + style?: Record; + /** Name of the attribute in data that should be copied to clipboard */ + color?: 'primary' | 'secondary'; + /** Name of the attribute in data that should be copied to clipboard */ + copyToClipboardAttr?: string; +}; +export type BackEndCommandJsonFormOptions = { + /** Data for the form */ + data?: JsonFormData; + /** Form title */ + title?: ioBroker.StringOrTranslated; + /** Buttons that will be shown on the bottom of the form */ + buttons?: (ActionButton | 'apply' | 'cancel' | 'close')[]; + /** Maximal form width */ + maxWidth?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + /** Minimal width of the dialog */ + minWidth?: number; + /** Do not translate title */ + noTranslation?: boolean; + /** Always allow the apply button. Even when nothing was changed */ + ignoreApplyDisabled?: boolean; +}; +export type JsonFormSchema = ConfigItemPanel | ConfigItemTabs; +export type JsonFormData = Record; +export interface DeviceDetails { + id: TId; + schema: JsonFormSchema; + data?: JsonFormData; +} +export {}; diff --git a/build/types/common.js b/build/types/common.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/build/types/common.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/build/types/errorCodes.d.ts b/build/types/errorCodes.d.ts new file mode 100644 index 0000000..2abdae6 --- /dev/null +++ b/build/types/errorCodes.d.ts @@ -0,0 +1,17 @@ +export declare enum ErrorCodes { + E_INSTANCE_ACTION_NOT_INITIALIZED = 101, + E_INSTANCE_ACTION_UNKNOWN = 102, + E_INSTANCE_ACTION_NO_HANDLER = 103, + E_DEVICE_ACTION_NOT_INITIALIZED = 201, + E_DEVICE_ACTION_DEVICE_UNKNOWN = 202, + E_DEVICE_ACTION_UNKNOWN = 203, + E_DEVICE_ACTION_NO_HANDLER = 204, + E_DEVICE_CONTROL_NOT_INITIALIZED = 301, + E_DEVICE_CONTROL_DEVICE_UNKNOWN = 302, + E_DEVICE_CONTROL_UNKNOWN = 303, + E_DEVICE_CONTROL_NO_HANDLER = 304, + E_DEVICE_GET_STATE_NOT_INITIALIZED = 401, + E_DEVICE_GET_STATE_DEVICE_UNKNOWN = 402, + E_DEVICE_GET_STATE_UNKNOWN = 403, + E_DEVICE_GET_STATE_NO_HANDLER = 404 +} diff --git a/build/types/errorCodes.js b/build/types/errorCodes.js new file mode 100644 index 0000000..c53793a --- /dev/null +++ b/build/types/errorCodes.js @@ -0,0 +1,21 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ErrorCodes = void 0; +var ErrorCodes; +(function (ErrorCodes) { + ErrorCodes[ErrorCodes["E_INSTANCE_ACTION_NOT_INITIALIZED"] = 101] = "E_INSTANCE_ACTION_NOT_INITIALIZED"; + ErrorCodes[ErrorCodes["E_INSTANCE_ACTION_UNKNOWN"] = 102] = "E_INSTANCE_ACTION_UNKNOWN"; + ErrorCodes[ErrorCodes["E_INSTANCE_ACTION_NO_HANDLER"] = 103] = "E_INSTANCE_ACTION_NO_HANDLER"; + ErrorCodes[ErrorCodes["E_DEVICE_ACTION_NOT_INITIALIZED"] = 201] = "E_DEVICE_ACTION_NOT_INITIALIZED"; + ErrorCodes[ErrorCodes["E_DEVICE_ACTION_DEVICE_UNKNOWN"] = 202] = "E_DEVICE_ACTION_DEVICE_UNKNOWN"; + ErrorCodes[ErrorCodes["E_DEVICE_ACTION_UNKNOWN"] = 203] = "E_DEVICE_ACTION_UNKNOWN"; + ErrorCodes[ErrorCodes["E_DEVICE_ACTION_NO_HANDLER"] = 204] = "E_DEVICE_ACTION_NO_HANDLER"; + ErrorCodes[ErrorCodes["E_DEVICE_CONTROL_NOT_INITIALIZED"] = 301] = "E_DEVICE_CONTROL_NOT_INITIALIZED"; + ErrorCodes[ErrorCodes["E_DEVICE_CONTROL_DEVICE_UNKNOWN"] = 302] = "E_DEVICE_CONTROL_DEVICE_UNKNOWN"; + ErrorCodes[ErrorCodes["E_DEVICE_CONTROL_UNKNOWN"] = 303] = "E_DEVICE_CONTROL_UNKNOWN"; + ErrorCodes[ErrorCodes["E_DEVICE_CONTROL_NO_HANDLER"] = 304] = "E_DEVICE_CONTROL_NO_HANDLER"; + ErrorCodes[ErrorCodes["E_DEVICE_GET_STATE_NOT_INITIALIZED"] = 401] = "E_DEVICE_GET_STATE_NOT_INITIALIZED"; + ErrorCodes[ErrorCodes["E_DEVICE_GET_STATE_DEVICE_UNKNOWN"] = 402] = "E_DEVICE_GET_STATE_DEVICE_UNKNOWN"; + ErrorCodes[ErrorCodes["E_DEVICE_GET_STATE_UNKNOWN"] = 403] = "E_DEVICE_GET_STATE_UNKNOWN"; + ErrorCodes[ErrorCodes["E_DEVICE_GET_STATE_NO_HANDLER"] = 404] = "E_DEVICE_GET_STATE_NO_HANDLER"; +})(ErrorCodes || (exports.ErrorCodes = ErrorCodes = {})); diff --git a/build/types/index.d.ts b/build/types/index.d.ts new file mode 100644 index 0000000..d150005 --- /dev/null +++ b/build/types/index.d.ts @@ -0,0 +1,4 @@ +export type * from './adapter'; +export { ACTIONS, type ChannelInfo, type Color, type ControlBase, type ControlState, type BackendToGuiCommand, type BackendToGuiCommandAllUpdate, type BackendToGuiCommandDeviceDelete, type BackendToGuiCommandDeviceStatusUpdate, type BackendToGuiCommandDeviceInfoUpdate, } from './base'; +export type * from './common'; +export * from './errorCodes'; diff --git a/build/types/index.js b/build/types/index.js new file mode 100644 index 0000000..9a2e036 --- /dev/null +++ b/build/types/index.js @@ -0,0 +1,20 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ACTIONS = void 0; +var base_1 = require("./base"); +Object.defineProperty(exports, "ACTIONS", { enumerable: true, get: function () { return base_1.ACTIONS; } }); +__exportStar(require("./errorCodes"), exports);