@iobroker/dm-utils 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 ioBroker Community Developers
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,322 @@
1
+ # dm-utils
2
+
3
+ Utility classes for ioBroker adapters to support [ioBroker.device-manager](https://github.com/ioBroker/ioBroker.device-manager).
4
+
5
+ ## How to use
6
+
7
+ Add in your `io-package.json` the property `deviceManager: true` to `common.supportedMessages`.
8
+ Note: If you don't have a `common.supportedMessages` property yet, you have to add it.
9
+ Also, if you don't have a `common.messagebox: true` property yet, you have to add it.
10
+
11
+ In your ioBroker adapter, add a subclass of `DeviceManagement` and override the methods you need (see next chapters):
12
+
13
+ Example:
14
+
15
+ - Create a subclass:
16
+
17
+ ```ts
18
+ class MyAdapterDeviceManagement extends DeviceManagement<MyAdapter> {
19
+ // contents see in the next chapters
20
+ }
21
+ ```
22
+
23
+ - Instantiate the subclass in your adapter class constructor:
24
+
25
+ ```ts
26
+ class MyAdapter extends utils.Adapter {
27
+ private readonly deviceManagement: MyAdapterDeviceManagement;
28
+
29
+ public constructor(options: Partial<utils.AdapterOptions> = {}) {
30
+ super({
31
+ ...options,
32
+ name: "my-adapter",
33
+ });
34
+ this.deviceManagement = new DmTestDeviceManagement(this);
35
+
36
+ // ... more code here
37
+ }
38
+ }
39
+ ```
40
+
41
+ ## Core concepts
42
+
43
+ ### Structure
44
+
45
+ In the UI, there are three levels of information:
46
+
47
+ - In the top level, a list of all adapter instances is shown (only containing adapter instances that support device management).
48
+ - Inside the adapter instance (when expanded), a list of devices is shown.
49
+ - Devices may contain additional details, which are shown when the row of the device is expanded.
50
+
51
+ ### Actions
52
+
53
+ The device manager tab allows the user to interact with the adapter instance in two ways:
54
+
55
+ - Actions per instance are shown above the list and should contain actions like "Search devices" or "Pair new device".
56
+ - Actions per device are shown in the device list inside an instance and should contain actions like "Edit settings" or "Remove".
57
+
58
+ When the user clicks on an action (i.e., a button in the UI),
59
+ the `DeviceManagement` implementation's `handleXxxAction()` is called, and the adapter can perform arbitrary actions
60
+ (see below for details).
61
+
62
+ ### Controls
63
+
64
+ 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.
65
+
66
+ When the user clicks on a control (i.e., a button in the UI),
67
+ the `DeviceManagement` implementation's `handleXxxAction()` is called, and the adapter can perform arbitrary actions
68
+ (see below for details).
69
+
70
+ ### Communication
71
+
72
+ The communication between the `ioBroker.device-manager` tab and the adapter happens through `sendTo`.
73
+
74
+ **IMPORTANT:** make sure your adapter doesn't handle `sendTo` messages starting with `dm:`, otherwise the communication will not work.
75
+
76
+ ### Access adapter methods
77
+
78
+ You can access all adapter methods like `getState()` or `getStateAsync()` via `this.adapter`.
79
+ Example: `this.getState()` -> `this.adapter.getState()`
80
+
81
+ ### Error Codes
82
+ | Code | Description |
83
+ |------|------------------------------------------------------------------------------------------------------------------------------|
84
+ | 101 | Instance action ${actionId} was called before getInstanceInfo() was called. This could happen if the instance has restarted. |
85
+ | 102 | Instance action ${actionId} is unknown. |
86
+ | 103 | Instance action ${actionId} is disabled because it has no handler. |
87
+ | 201 | Device action ${actionId} was called before listDevices() was called. This could happen if the instance has restarted. |
88
+ | 202 | Device action ${actionId} was called on unknown device: ${deviceId}. |
89
+ | 203 | Device action ${actionId} doesn't exist on device ${deviceId}. |
90
+ | 204 | Device action ${actionId} on ${deviceId} is disabled because it has no handler. |
91
+
92
+ ## Examples
93
+
94
+ To get an idea of how to use `dm-utils`, please have a look at:
95
+ - [the folder "examples"](examples/dm-test.ts) or
96
+ - [ioBroker.dm-test](https://github.com/UncleSamSwiss/ioBroker.dm-test)
97
+
98
+ ## `DeviceManagement` methods to override
99
+
100
+ All methods can either return an object of the defined value or a `Promise` resolving to the object.
101
+
102
+ This allows you to implement the method synchronously or asynchronously, depending on your implementation.
103
+
104
+ ### `listDevices()`
105
+
106
+ This method must always be overridden (as it is abstract in the base class).
107
+
108
+ You must return an array with information about all devices of this adapter's instance.
109
+
110
+ This method is called when the user expands an instance in the list.
111
+
112
+ In most cases, you will get all states of your instance and fill the array with the relevant information.
113
+
114
+ Every array entry is an object of type `DeviceInfo` which has the following properties:
115
+
116
+ - `id` (string): a unique (human readable) identifier of the device (it must be unique for your adapter instance only)
117
+ - `name` (string or translations): the human-readable name of this device
118
+ - `status` (optional): the current status of the device, which can be one of:
119
+ - `"disconnected"`
120
+ - `"connected"`
121
+ - an object containing:
122
+ - `icon` (string): an icon depicting the status of the device (see below for details)
123
+ - `description` (string, optional): a text that will be shown as a tooltip on the status
124
+ - `actions` (array, optional): an array of actions that can be performed on the device; each object contains:
125
+ - `id` (string): unique identifier to recognize an action (never shown to the user)
126
+ - `icon` (string): an icon shown on the button (see below for details)
127
+ - `description` (string, optional): a text that will be shown as a tooltip on the button
128
+ - `disabled` (boolean, optional): if set to `true`, the button can't be clicked but is shown to the user
129
+ - `hasDetails` (boolean, optional): if set to `true`, the row of the device can be expanded and details are shown below
130
+
131
+ ### `getInstanceInfo()`
132
+
133
+ This method allows the device manager tab to gather some general information about the instance. It is called when the user opens the tab.
134
+
135
+ If you override this method, the returned object must contain:
136
+
137
+ - `apiVersion` (string): the supported API version; must currently always be `"v1"`
138
+ - `actions` (array, optional): an array of actions that can be performed on the instance; each object contains:
139
+ - `id` (string): unique identifier to recognize an action (never shown to the user)
140
+ - `icon` (string): an icon shown on the button (see below for details)
141
+ - `title` (string): the title shown next to the icon on the button
142
+ - `description` (string, optional): a text that will be shown as a tooltip on the button
143
+ - `disabled` (boolean, optional): if set to `true`, the button can't be clicked but is shown to the user
144
+
145
+ ### `getDeviceDetails(id: string)`
146
+
147
+ This method is called if a device's `hasDetails` is set to `true` and the user clicks on the expander.
148
+
149
+ The returned object must contain:
150
+
151
+ - `id` (string): the `id` given as parameter to the method call
152
+ - `schema` (Custom JSON form schema): the schema of the Custom JSON form to show below the device information
153
+ - `data` (object, optional): the data used to populate the Custom JSON form
154
+
155
+ For more details about the schema, see [here](https://github.com/ioBroker/ioBroker.admin/blob/master/src-rx/src/components/JsonConfigComponent/SCHEMA.md).
156
+
157
+ 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<xxx>` objects to send data to the adapter.
158
+
159
+ ### `handleInstanceAction(actionId: string, context: ActionContext)
160
+
161
+ This method is called when to user clicks on an action (i.e., button) for an adapter instance.
162
+
163
+ The parameters of this method are:
164
+ - `actionId` (string): the `id` that was given in `getInstanceInfo()` --> `actions[].id`
165
+ - `context` (object): object containing helper methods that can be used when executing the action
166
+
167
+ The returned object must contain:
168
+ - `refresh` (boolean): set this to `true` if you want the list to be reloaded after this action
169
+
170
+ This method can be implemented asynchronously and can take a lot of time to complete.
171
+
172
+ See below for how to interact with the user.
173
+
174
+ ### `handleDeviceAction(deviceId: string, actionId: string, context: ActionContext)
175
+
176
+ This method is called when the user clicks on an action (i.e., button) for a device.
177
+
178
+ The parameters of this method are:
179
+ - `deviceId` (string): the `id` that was given in `listDevices()` --> `[].id`
180
+ - `actionId` (string): the `id` that was given in `listDevices()` --> `[].actions[].id`
181
+ - `context` (object): object containing helper methods that can be used when executing the action
182
+
183
+ The returned object must contain:
184
+ - `refresh` (string / boolean): the following values are allowed:
185
+ - `"device"`: if you want the device details to be reloaded after this action
186
+ - `"instance"`: if you want the entire device list to be reloaded after this action
187
+ - `false`: if you don't want anything to be refreshed (important: this is a boolean, not a string!)
188
+
189
+ This method can be implemented asynchronously and can take a lot of time to complete.
190
+
191
+ See below for how to interact with the user.
192
+
193
+ ### `handleDeviceControl(deviceId: string, controlId: string, state: ControlState, context: MessageContext)
194
+
195
+ This method is called when the user clicks on a control (i.e., slider) in the device card.
196
+
197
+ The parameters of this method are:
198
+ - `deviceId` (string): the `id` that was given in `listDevices()` --> `[].id`
199
+ - `controlId` (string): the `id` that was given in `listDevices()` --> `[].controls[].id`
200
+ - `state` (string | number | boolean): new state for the control, that will be sent to real device
201
+ - `context` (object): object containing helper methods that can be used when executing the action
202
+
203
+ The returned object must contain:
204
+ - `state`: ioBroker state object
205
+
206
+ This method can be implemented asynchronously and can take a lot of time to complete.
207
+
208
+ ### `handleDeviceControlState(deviceId: string, controlId: string, context: MessageContext)
209
+
210
+ This method is called when GUI requests the update of the state.
211
+
212
+ The parameters of this method are:
213
+ - `deviceId` (string): the `id` that was given in `listDevices()` --> `[].id`
214
+ - `controlId` (string): the `id` that was given in `listDevices()` --> `[].controls[].id`
215
+ - `context` (object): object containing helper methods that can be used when executing the action
216
+
217
+ The returned object must contain:
218
+ - `state`: ioBroker state object
219
+
220
+ This method can be implemented asynchronously and can take a lot of time to complete.
221
+
222
+ ## Action sequences
223
+
224
+ To allow your adapter to interact with the user, you can use "actions".
225
+
226
+ As described above, there are actions on the instance and on devices. The behavior of both methods is similar.
227
+
228
+ Inside an action method (`handleInstanceAction()` or `handleDeviceAction()`) you can perform arbitrary actions, like talking to a device or API, and you can interact with the user. For interactions, there are methods you can call on `context`:
229
+
230
+ ### `showMessage(text: ioBroker.StringOrTranslated)`
231
+
232
+ Shows a message to the user.
233
+
234
+ The method has the following parameter:
235
+ - `text` (string or translation): the text to show to the user
236
+
237
+ This asynchronous method returns (or rather: the Promise is resolved) once the user has clicked on "OK".
238
+
239
+ ### `showConfirmation(text: ioBroker.StringOrTranslated)`
240
+
241
+ Lets the user confirm an action by showing a message with an "OK" and "Cancel" button.
242
+
243
+ The method has the following parameter:
244
+ - `text` (string or translation): the text to show to the user
245
+
246
+ This asynchronous method returns (or rather: the Promise is resolved) once the user has clicked a button in the dialog:
247
+ - `true` if the user clicked "OK"
248
+ - `false` if the user clicked "Cancel"
249
+
250
+ ### `showForm(schema: JsonFormSchema, options?: { data?: JsonFormData; title?: string })`
251
+
252
+ Shows a dialog with a Custom JSON form that can be edited by the user.
253
+
254
+ The method has the following parameters:
255
+ - `schema` (Custom JSON form schema): the schema of the Custom JSON form to show in the dialog
256
+ - `options` (object, optional): options to configure the dialog further
257
+ - `data` (object, optional): the data used to populate the Custom JSON form
258
+ - `title` (string, optional): the dialog title
259
+
260
+ This asynchronous method returns (or rather: the Promise is resolved) once the user has clicked a button in the dialog:
261
+ - the form data, if the user clicked "OK"
262
+ - `undefined`, if the user clicked "Cancel"
263
+
264
+ ### `openProgress(title: string, options?: {indeterminate?: boolean, value?: number, label?: string})`
265
+
266
+ Shows a dialog with a linear progress bar to the user. There is no way for the user to dismiss this dialog.
267
+
268
+ The method has the following parameters:
269
+ - `title` (string): the dialog title
270
+ - `options` (object, optional): options to configure the dialog further
271
+ - `indeterminate` (boolean, optional): set to `true` to visualize an unspecified wait time
272
+ - `value` (number, optional): the progress value to show to the user (if set, it must be a value between 0 and 100)
273
+ - `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...")
274
+
275
+ This method returns a promise that resolves to a `ProgressDialog` object.
276
+
277
+ **Important:** you must always call `close()` on the returned object before you may open any other dialog.
278
+
279
+ `ProgressDialog` has two methods:
280
+
281
+ - `update(update: { title?: string; indeterminate?: boolean; value?:number; label?: string; })`
282
+ - Updates the progress dialog with new values
283
+ - The method has the following parameter:
284
+ - `update` (object): what to update in the dialog
285
+ - `title` (string, optional): change the dialog title
286
+ - `indeterminate` (boolean, optional): change whether the progress is indeterminate
287
+ - `value` (number, optional): change the progress value
288
+ - `label` (string, optional): change the label to the right of the progress bar
289
+ - `close()`
290
+ - Closes the progress dialog (and allows you to open other dialogs)
291
+
292
+ <!--
293
+ Placeholder for the next version (at the beginning of the line):
294
+ ### **WORK IN PROGRESS**
295
+ -->
296
+ ## Changelog
297
+ ### 0.1.2 (2023-12-10)
298
+ * (bluefox) added some fields to DeviceInfo interface
299
+ * (bluefox) added control possibilities
300
+
301
+ ## License
302
+ MIT License
303
+
304
+ Copyright (c) 2023 ioBroker Community Developers
305
+
306
+ Permission is hereby granted, free of charge, to any person obtaining a copy
307
+ of this software and associated documentation files (the "Software"), to deal
308
+ in the Software without restriction, including without limitation the rights
309
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
310
+ copies of the Software, and to permit persons to whom the Software is
311
+ furnished to do so, subject to the following conditions:
312
+
313
+ The above copyright notice and this permission notice shall be included in all
314
+ copies or substantial portions of the Software.
315
+
316
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
317
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
318
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
319
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
320
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
321
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
322
+ SOFTWARE.
@@ -0,0 +1,15 @@
1
+ import { JsonFormData, JsonFormSchema } from ".";
2
+ import { ProgressDialog } from "./ProgressDialog";
3
+ export interface ActionContext {
4
+ showMessage(text: ioBroker.StringOrTranslated): Promise<void>;
5
+ showConfirmation(text: ioBroker.StringOrTranslated): Promise<boolean>;
6
+ showForm(schema: JsonFormSchema, options?: {
7
+ data?: JsonFormData;
8
+ title?: ioBroker.StringOrTranslated;
9
+ }): Promise<JsonFormData | undefined>;
10
+ openProgress(title: string, options?: {
11
+ indeterminate?: boolean;
12
+ value?: number;
13
+ label?: ioBroker.StringOrTranslated;
14
+ }): Promise<ProgressDialog>;
15
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,50 @@
1
+ import { AdapterInstance } from "@iobroker/adapter-core";
2
+ import { ActionContext } from "./ActionContext";
3
+ import { ProgressDialog } from "./ProgressDialog";
4
+ import { DeviceDetails, DeviceInfo, ErrorResponse, InstanceDetails, JsonFormData, JsonFormSchema, RefreshResponse, RetVal } from "./types";
5
+ import { ControlState } from "./types/base";
6
+ export declare abstract class DeviceManagement<T extends AdapterInstance = AdapterInstance> {
7
+ protected readonly adapter: T;
8
+ private instanceInfo?;
9
+ private devices?;
10
+ private readonly contexts;
11
+ constructor(adapter: T);
12
+ protected get log(): ioBroker.Log;
13
+ protected getInstanceInfo(): RetVal<InstanceDetails>;
14
+ protected abstract listDevices(): RetVal<DeviceInfo[]>;
15
+ protected getDeviceDetails(id: string): RetVal<DeviceDetails | null | {
16
+ error: string;
17
+ }>;
18
+ protected handleInstanceAction(actionId: string, context: ActionContext): RetVal<ErrorResponse> | RetVal<RefreshResponse>;
19
+ protected handleDeviceAction(deviceId: string, actionId: string, context: ActionContext): RetVal<ErrorResponse> | RetVal<RefreshResponse>;
20
+ protected handleDeviceControl(deviceId: string, controlId: string, newState: ControlState, context: MessageContext): RetVal<ErrorResponse | ioBroker.State>;
21
+ protected handleDeviceControlState(deviceId: string, controlId: string, context: MessageContext): RetVal<ErrorResponse | ioBroker.State>;
22
+ private onMessage;
23
+ private handleMessage;
24
+ private convertActions;
25
+ private convertControls;
26
+ private sendReply;
27
+ }
28
+ export declare class MessageContext implements ActionContext {
29
+ private readonly adapter;
30
+ private hasOpenProgressDialog;
31
+ private lastMessage?;
32
+ private progressHandler?;
33
+ constructor(msg: ioBroker.Message, adapter: AdapterInstance);
34
+ showMessage(text: ioBroker.StringOrTranslated): Promise<void>;
35
+ showConfirmation(text: ioBroker.StringOrTranslated): Promise<boolean>;
36
+ showForm(schema: JsonFormSchema, options?: {
37
+ data?: JsonFormData;
38
+ title?: string;
39
+ }): Promise<JsonFormData | undefined>;
40
+ openProgress(title: string, options?: {
41
+ indeterminate?: boolean;
42
+ value?: number;
43
+ label?: string;
44
+ }): Promise<ProgressDialog>;
45
+ sendFinalResult(result: ErrorResponse | RefreshResponse): void;
46
+ sendControlResult(deviceId: string, controlId: string, result: ErrorResponse | ioBroker.State): void;
47
+ handleProgress(message: ioBroker.Message): void;
48
+ private checkPreconditions;
49
+ private send;
50
+ }
@@ -0,0 +1,359 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MessageContext = exports.DeviceManagement = void 0;
4
+ class DeviceManagement {
5
+ constructor(adapter) {
6
+ this.adapter = adapter;
7
+ this.contexts = new Map();
8
+ adapter.on("message", this.onMessage.bind(this));
9
+ }
10
+ get log() {
11
+ return this.adapter.log;
12
+ }
13
+ getInstanceInfo() {
14
+ return { apiVersion: "v1" };
15
+ }
16
+ getDeviceDetails(id) {
17
+ return { id, schema: {} };
18
+ }
19
+ handleInstanceAction(actionId, context) {
20
+ var _a;
21
+ if (!this.instanceInfo) {
22
+ this.log.warn(`Instance action ${actionId} was called before getInstanceInfo()`);
23
+ return { error: { code: 101, message: `Instance action ${actionId} was called before getInstanceInfo()` } };
24
+ }
25
+ const action = (_a = this.instanceInfo.actions) === null || _a === void 0 ? void 0 : _a.find((a) => a.id === actionId);
26
+ if (!action) {
27
+ this.log.warn(`Instance action ${actionId} is unknown`);
28
+ return { error: { code: 102, message: `Instance action ${actionId} is unknown` } };
29
+ }
30
+ if (!action.handler) {
31
+ this.log.warn(`Instance action ${actionId} is disabled because it has no handler`);
32
+ return {
33
+ error: { code: 103, message: `Instance action ${actionId} is disabled because it has no handler` },
34
+ };
35
+ }
36
+ return action.handler(context);
37
+ }
38
+ handleDeviceAction(deviceId, actionId, context) {
39
+ var _a;
40
+ if (!this.devices) {
41
+ this.log.warn(`Device action ${actionId} was called before listDevices()`);
42
+ return { error: { code: 201, message: `Device action ${actionId} was called before listDevices()` } };
43
+ }
44
+ const device = this.devices.get(deviceId);
45
+ if (!device) {
46
+ this.log.warn(`Device action ${actionId} was called on unknown device: ${deviceId}`);
47
+ return {
48
+ error: { code: 202, message: `Device action ${actionId} was called on unknown device: ${deviceId}` },
49
+ };
50
+ }
51
+ const action = (_a = device.actions) === null || _a === void 0 ? void 0 : _a.find((a) => a.id === actionId);
52
+ if (!action) {
53
+ this.log.warn(`Device action ${actionId} doesn't exist on device ${deviceId}`);
54
+ return { error: { code: 203, message: `Device action ${actionId} doesn't exist on device ${deviceId}` } };
55
+ }
56
+ if (!action.handler) {
57
+ this.log.warn(`Device action ${actionId} on ${deviceId} is disabled because it has no handler`);
58
+ return {
59
+ error: {
60
+ code: 204,
61
+ message: `Device action ${actionId} on ${deviceId} is disabled because it has no handler`,
62
+ },
63
+ };
64
+ }
65
+ return action.handler(deviceId, context);
66
+ }
67
+ handleDeviceControl(deviceId, controlId, newState, context) {
68
+ var _a;
69
+ if (!this.devices) {
70
+ this.log.warn(`Device control ${controlId} was called before listDevices()`);
71
+ return { error: { code: 201, message: `Device control ${controlId} was called before listDevices()` } };
72
+ }
73
+ const device = this.devices.get(deviceId);
74
+ if (!device) {
75
+ this.log.warn(`Device control ${controlId} was called on unknown device: ${deviceId}`);
76
+ return {
77
+ error: { code: 202, message: `Device control ${controlId} was called on unknown device: ${deviceId}` },
78
+ };
79
+ }
80
+ const control = (_a = device.controls) === null || _a === void 0 ? void 0 : _a.find((a) => a.id === controlId);
81
+ if (!control) {
82
+ this.log.warn(`Device control ${controlId} doesn't exist on device ${deviceId}`);
83
+ return { error: { code: 203, message: `Device control ${controlId} doesn't exist on device ${deviceId}` } };
84
+ }
85
+ if (!control.handler) {
86
+ this.log.warn(`Device control ${controlId} on ${deviceId} is disabled because it has no handler`);
87
+ return {
88
+ error: {
89
+ code: 204,
90
+ message: `Device control ${controlId} on ${deviceId} is disabled because it has no handler`,
91
+ },
92
+ };
93
+ }
94
+ return control.handler(deviceId, controlId, newState, context);
95
+ }
96
+ // request state of control
97
+ handleDeviceControlState(deviceId, controlId, context) {
98
+ var _a;
99
+ if (!this.devices) {
100
+ this.log.warn(`Device control ${controlId} was called before listDevices()`);
101
+ return { error: { code: 201, message: `Device control ${controlId} was called before listDevices()` } };
102
+ }
103
+ const device = this.devices.get(deviceId);
104
+ if (!device) {
105
+ this.log.warn(`Device control ${controlId} was called on unknown device: ${deviceId}`);
106
+ return {
107
+ error: { code: 202, message: `Device control ${controlId} was called on unknown device: ${deviceId}` },
108
+ };
109
+ }
110
+ const control = (_a = device.controls) === null || _a === void 0 ? void 0 : _a.find((a) => a.id === controlId);
111
+ if (!control) {
112
+ this.log.warn(`Device control ${controlId} doesn't exist on device ${deviceId}`);
113
+ return { error: { code: 203, message: `Device control ${controlId} doesn't exist on device ${deviceId}` } };
114
+ }
115
+ if (!control.handler) {
116
+ this.log.warn(`Device control ${controlId} on ${deviceId} is disabled because it has no handler`);
117
+ return {
118
+ error: {
119
+ code: 204,
120
+ message: `Device control ${controlId} on ${deviceId} is disabled because it has no handler`,
121
+ },
122
+ };
123
+ }
124
+ return control.getStateHandler(deviceId, controlId, context);
125
+ }
126
+ onMessage(obj) {
127
+ if (!obj.command.startsWith("dm:")) {
128
+ return;
129
+ }
130
+ this.handleMessage(obj).catch(this.log.error);
131
+ }
132
+ async handleMessage(msg) {
133
+ this.log.debug(`DeviceManagement received: ${JSON.stringify(msg)}`);
134
+ switch (msg.command) {
135
+ case "dm:instanceInfo": {
136
+ this.instanceInfo = await this.getInstanceInfo();
137
+ this.sendReply(Object.assign(Object.assign({}, this.instanceInfo), { actions: this.convertActions(this.instanceInfo.actions) }), msg);
138
+ return;
139
+ }
140
+ case "dm:listDevices": {
141
+ const deviceList = await this.listDevices();
142
+ this.devices = deviceList.reduce((map, value) => {
143
+ if (map.has(value.id)) {
144
+ throw new Error(`Device ID ${value.id} is not unique`);
145
+ }
146
+ map.set(value.id, value);
147
+ return map;
148
+ }, new Map());
149
+ const apiDeviceList = deviceList.map((d) => (Object.assign(Object.assign({}, d), { actions: this.convertActions(d.actions), controls: this.convertControls(d.controls) })));
150
+ this.sendReply(apiDeviceList, msg);
151
+ this.adapter.sendTo(msg.from, msg.command, this.devices, msg.callback);
152
+ return;
153
+ }
154
+ case "dm:deviceDetails": {
155
+ const details = await this.getDeviceDetails(msg.message);
156
+ this.adapter.sendTo(msg.from, msg.command, details, msg.callback);
157
+ return;
158
+ }
159
+ case "dm:instanceAction": {
160
+ const action = msg.message;
161
+ const context = new MessageContext(msg, this.adapter);
162
+ this.contexts.set(msg._id, context);
163
+ const result = await this.handleInstanceAction(action.actionId, context);
164
+ this.contexts.delete(msg._id);
165
+ context.sendFinalResult(result);
166
+ return;
167
+ }
168
+ case "dm:deviceAction": {
169
+ const action = msg.message;
170
+ const context = new MessageContext(msg, this.adapter);
171
+ this.contexts.set(msg._id, context);
172
+ const result = await this.handleDeviceAction(action.deviceId, action.actionId, context);
173
+ this.contexts.delete(msg._id);
174
+ context.sendFinalResult(result);
175
+ return;
176
+ }
177
+ case "dm:deviceControl": {
178
+ const control = msg.message;
179
+ const context = new MessageContext(msg, this.adapter);
180
+ this.contexts.set(msg._id, context);
181
+ const result = await this.handleDeviceControl(control.deviceId, control.controlId, control.state, context);
182
+ this.contexts.delete(msg._id);
183
+ context.sendControlResult(control.deviceId, control.controlId, result);
184
+ return;
185
+ }
186
+ case "dm:deviceControlState": {
187
+ const control = msg.message;
188
+ const context = new MessageContext(msg, this.adapter);
189
+ this.contexts.set(msg._id, context);
190
+ const result = await this.handleDeviceControlState(control.deviceId, control.controlId, context);
191
+ this.contexts.delete(msg._id);
192
+ context.sendControlResult(control.deviceId, control.controlId, result);
193
+ return;
194
+ }
195
+ case "dm:actionProgress": {
196
+ const { origin } = msg.message;
197
+ const context = this.contexts.get(origin);
198
+ if (!context) {
199
+ this.log.warn(`Unknown message origin: ${origin}`);
200
+ this.sendReply({ error: "Unknown action origin" }, msg);
201
+ return;
202
+ }
203
+ context.handleProgress(msg);
204
+ return;
205
+ }
206
+ }
207
+ }
208
+ convertActions(actions) {
209
+ if (!actions) {
210
+ return undefined;
211
+ }
212
+ // detect duplicate IDs
213
+ const ids = new Set();
214
+ actions.forEach((a) => {
215
+ if (ids.has(a.id)) {
216
+ throw new Error(`Action ID ${a.id} is used twice, this would lead to unexpected behavior`);
217
+ }
218
+ ids.add(a.id);
219
+ });
220
+ // remove handler function to send it as JSON
221
+ return actions.map((a) => (Object.assign(Object.assign({}, a), { handler: undefined, disabled: !a.handler })));
222
+ }
223
+ convertControls(controls) {
224
+ if (!controls) {
225
+ return undefined;
226
+ }
227
+ // detect duplicate IDs
228
+ const ids = new Set();
229
+ controls.forEach((a) => {
230
+ if (ids.has(a.id)) {
231
+ throw new Error(`Control ID ${a.id} is used twice, this would lead to unexpected behavior`);
232
+ }
233
+ ids.add(a.id);
234
+ });
235
+ // remove handler function to send it as JSON
236
+ return controls.map((a) => (Object.assign(Object.assign({}, a), { handler: undefined, getStateHandler: undefined })));
237
+ }
238
+ sendReply(reply, msg) {
239
+ this.adapter.sendTo(msg.from, msg.command, reply, msg.callback);
240
+ }
241
+ }
242
+ exports.DeviceManagement = DeviceManagement;
243
+ class MessageContext {
244
+ constructor(msg, adapter) {
245
+ this.adapter = adapter;
246
+ this.hasOpenProgressDialog = false;
247
+ this.lastMessage = msg;
248
+ }
249
+ showMessage(text) {
250
+ this.checkPreconditions();
251
+ const promise = new Promise((resolve) => {
252
+ this.progressHandler = () => resolve();
253
+ });
254
+ this.send("message", {
255
+ message: text,
256
+ });
257
+ return promise;
258
+ }
259
+ showConfirmation(text) {
260
+ this.checkPreconditions();
261
+ const promise = new Promise((resolve) => {
262
+ this.progressHandler = (msg) => resolve(!!msg.confirm);
263
+ });
264
+ this.send("confirm", {
265
+ confirm: text,
266
+ });
267
+ return promise;
268
+ }
269
+ showForm(schema, options) {
270
+ this.checkPreconditions();
271
+ const promise = new Promise((resolve) => {
272
+ this.progressHandler = (msg) => resolve(msg.data);
273
+ });
274
+ this.send("form", {
275
+ form: Object.assign({ schema }, options),
276
+ });
277
+ return promise;
278
+ }
279
+ openProgress(title, options) {
280
+ this.checkPreconditions();
281
+ this.hasOpenProgressDialog = true;
282
+ const dialog = {
283
+ update: (update) => {
284
+ const promise = new Promise((resolve) => {
285
+ this.progressHandler = () => resolve();
286
+ });
287
+ this.send("progress", {
288
+ progress: Object.assign(Object.assign(Object.assign({ title }, options), update), { open: true }),
289
+ });
290
+ return promise;
291
+ },
292
+ close: () => {
293
+ const promise = new Promise((resolve) => {
294
+ this.progressHandler = () => {
295
+ this.hasOpenProgressDialog = false;
296
+ resolve();
297
+ };
298
+ });
299
+ this.send("progress", {
300
+ progress: { open: false },
301
+ });
302
+ return promise;
303
+ },
304
+ };
305
+ const promise = new Promise((resolve) => {
306
+ this.progressHandler = () => resolve(dialog);
307
+ });
308
+ this.send("progress", {
309
+ progress: Object.assign(Object.assign({ title }, options), { open: true }),
310
+ });
311
+ return promise;
312
+ }
313
+ sendFinalResult(result) {
314
+ this.send("result", {
315
+ result,
316
+ });
317
+ }
318
+ sendControlResult(deviceId, controlId, result) {
319
+ if (typeof result === "object" && "error" in result) {
320
+ this.send("result", {
321
+ result: {
322
+ error: result.error,
323
+ deviceId,
324
+ controlId,
325
+ },
326
+ });
327
+ }
328
+ else {
329
+ this.send("result", {
330
+ result: {
331
+ state: result,
332
+ deviceId,
333
+ controlId,
334
+ },
335
+ });
336
+ }
337
+ }
338
+ handleProgress(message) {
339
+ const currentHandler = this.progressHandler;
340
+ if (currentHandler && typeof message.message !== "string") {
341
+ this.lastMessage = message;
342
+ this.progressHandler = undefined;
343
+ currentHandler(message.message);
344
+ }
345
+ }
346
+ checkPreconditions() {
347
+ if (this.hasOpenProgressDialog) {
348
+ throw new Error("Can't show another dialog while a progress dialog is open. Please call 'close()' on the dialog before opening another dialog.");
349
+ }
350
+ }
351
+ send(type, message) {
352
+ if (!this.lastMessage) {
353
+ throw new Error("No outstanding message, can't send a new one");
354
+ }
355
+ this.adapter.sendTo(this.lastMessage.from, this.lastMessage.command, Object.assign(Object.assign({}, message), { type, origin: this.lastMessage.message.origin || this.lastMessage._id }), this.lastMessage.callback);
356
+ this.lastMessage = undefined;
357
+ }
358
+ }
359
+ exports.MessageContext = MessageContext;
@@ -0,0 +1,9 @@
1
+ export interface ProgressDialog {
2
+ update(update: {
3
+ title?: ioBroker.StringOrTranslated;
4
+ indeterminate?: boolean;
5
+ value?: number;
6
+ label?: ioBroker.StringOrTranslated;
7
+ }): Promise<void>;
8
+ close(): Promise<void>;
9
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,3 @@
1
+ export * from "./ActionContext";
2
+ export * from "./DeviceManagement";
3
+ export * from "./types";
package/build/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./ActionContext"), exports);
18
+ __exportStar(require("./DeviceManagement"), exports);
19
+ // don't export * from "./MessageContext" as it is private
20
+ __exportStar(require("./types"), exports);
@@ -0,0 +1,7 @@
1
+ import * as base from "./base";
2
+ export type ActionBase = base.ActionBase<"adapter">;
3
+ export type InstanceAction = base.InstanceAction<"adapter">;
4
+ export type DeviceAction = base.DeviceAction<"adapter">;
5
+ export type InstanceDetails = base.InstanceDetails<"adapter">;
6
+ export type DeviceInfo = base.DeviceInfo<"adapter">;
7
+ export type DeviceControl = base.DeviceControl<"adapter">;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,7 @@
1
+ import * as base from "./base";
2
+ export type ActionBase = base.ActionBase<"api">;
3
+ export type InstanceAction = base.InstanceAction<"api">;
4
+ export type DeviceAction = base.DeviceAction<"api">;
5
+ export type InstanceDetails = base.InstanceDetails<"api">;
6
+ export type DeviceInfo = base.DeviceInfo<"api">;
7
+ export type DeviceControl = base.DeviceControl<"api">;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,71 @@
1
+ import { ActionContext, ErrorResponse, MessageContext } from "..";
2
+ import { ApiVersion, DeviceRefresh, DeviceStatus, RetVal } from "./common";
3
+ type ActionType = "api" | "adapter";
4
+ export type Color = "primary" | "secondary" | string & {};
5
+ export type ControlState = string | number | boolean | null;
6
+ export interface ActionBase<T extends ActionType> {
7
+ id: string;
8
+ /**
9
+ * This can either be base64 or the URL to an icon.
10
+ */
11
+ icon: string;
12
+ description?: ioBroker.StringOrTranslated;
13
+ disabled?: T extends "api" ? boolean : never;
14
+ color?: Color;
15
+ backgroundColor?: Color;
16
+ }
17
+ export interface ControlBase {
18
+ id: string;
19
+ type: "button" | "switch" | "slider" | "select" | "icon" | "color";
20
+ state?: ioBroker.State;
21
+ stateId?: string;
22
+ icon?: string;
23
+ iconOn?: string;
24
+ min?: number;
25
+ max?: number;
26
+ label?: ioBroker.StringOrTranslated;
27
+ labelOn?: ioBroker.StringOrTranslated;
28
+ description?: ioBroker.StringOrTranslated;
29
+ color?: Color;
30
+ colorOn?: Color;
31
+ controlDelay?: number;
32
+ options?: {
33
+ label: ioBroker.StringOrTranslated;
34
+ value: ControlState;
35
+ icon?: string;
36
+ color?: Color;
37
+ }[];
38
+ }
39
+ export interface DeviceControl<T extends ActionType = "api"> extends ActionBase<T> {
40
+ handler?: T extends "api" ? never : (deviceId: string, actionId: string, state: ControlState, context: MessageContext) => RetVal<ErrorResponse | ioBroker.State>;
41
+ getStateHandler?: T extends "api" ? never : (deviceId: string, actionId: string, context: MessageContext) => RetVal<ErrorResponse | ioBroker.State>;
42
+ }
43
+ export interface InstanceAction<T extends ActionType = "api"> extends ActionBase<T> {
44
+ handler?: T extends "api" ? never : (context: ActionContext) => RetVal<{
45
+ refresh: boolean;
46
+ }>;
47
+ title: ioBroker.StringOrTranslated;
48
+ }
49
+ export interface DeviceAction<T extends ActionType = "api"> extends ActionBase<T> {
50
+ handler?: T extends "api" ? never : (deviceId: string, context: ActionContext) => RetVal<{
51
+ refresh: DeviceRefresh;
52
+ }>;
53
+ }
54
+ export interface InstanceDetails<T extends ActionType = "api"> {
55
+ apiVersion: ApiVersion;
56
+ actions?: InstanceAction<T>[];
57
+ }
58
+ export interface DeviceInfo<T extends ActionType = "api"> {
59
+ id: string;
60
+ icon?: string;
61
+ manufacturer?: ioBroker.StringOrTranslated;
62
+ model?: ioBroker.StringOrTranslated;
63
+ color?: Color;
64
+ backgroundColor?: Color;
65
+ name: ioBroker.StringOrTranslated;
66
+ status?: DeviceStatus | DeviceStatus[];
67
+ actions?: DeviceAction<T>[];
68
+ controls?: DeviceControl<T>[];
69
+ hasDetails?: boolean;
70
+ }
71
+ export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,29 @@
1
+ export type ApiVersion = "v1";
2
+ export type DeviceStatus = "connected" | "disconnected" | {
3
+ /**
4
+ * This can either be the name of a font awesome icon (e.g. "fa-signal") or the URL to an icon.
5
+ */
6
+ icon?: string;
7
+ battery?: number | boolean | "charging" | string;
8
+ connection: "connected" | "disconnected";
9
+ rssi?: number;
10
+ warning?: ioBroker.StringOrTranslated | boolean;
11
+ };
12
+ export type DeviceRefresh = "device" | "instance" | false | true;
13
+ export type RefreshResponse = {
14
+ refresh: DeviceRefresh;
15
+ };
16
+ export type ErrorResponse = {
17
+ error: {
18
+ code: number;
19
+ message: string;
20
+ };
21
+ };
22
+ export type RetVal<T> = T | Promise<T>;
23
+ export type JsonFormSchema = Record<string, any>;
24
+ export type JsonFormData = Record<string, any>;
25
+ export interface DeviceDetails {
26
+ id: string;
27
+ schema: JsonFormSchema;
28
+ data?: JsonFormData;
29
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ export * from "./adapter";
2
+ export * from "./common";
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./adapter"), exports);
18
+ __exportStar(require("./common"), exports);
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@iobroker/dm-utils",
3
+ "version": "0.1.2",
4
+ "description": "ioBroker Device Manager utilities for backend",
5
+ "main": "build/index.js",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "types": "build/index.d.ts",
10
+ "scripts": {
11
+ "build": "tsc -p tsconfig.json",
12
+ "lint": "eslint .",
13
+ "prettier": "prettier -u -w examples src",
14
+ "release": "release-script",
15
+ "release-patch": "release-script patch --yes",
16
+ "release-minor": "release-script minor --yes",
17
+ "release-major": "release-script major --yes",
18
+ "update-packages": "ncu --upgrade",
19
+ "npm": "npm i"
20
+ },
21
+ "author": "UncleSamSwiss",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "@iobroker/adapter-core": "^3.0.4"
25
+ },
26
+ "devDependencies": {
27
+ "@alcalzone/release-script": "^3.7.0",
28
+ "@alcalzone/release-script-plugin-license": "^3.7.0",
29
+ "@types/node": "^20.10.4",
30
+ "@typescript-eslint/eslint-plugin": "^6.13.2",
31
+ "@typescript-eslint/parser": "^6.13.2",
32
+ "eslint": "^8.55.0",
33
+ "eslint-config-prettier": "^9.1.0",
34
+ "prettier": "^3.1.1",
35
+ "typescript": "^5.3.3"
36
+ },
37
+ "files": [
38
+ "LICENSE",
39
+ "README.md",
40
+ "build"
41
+ ]
42
+ }