@ecobridge.xyz/devicemanager 3.0.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.
Files changed (137) hide show
  1. package/.gitea/workflows/default_nottags.yaml +66 -0
  2. package/.gitea/workflows/default_tags.yaml +124 -0
  3. package/.vscode/launch.json +11 -0
  4. package/.vscode/settings.json +26 -0
  5. package/changelog.md +91 -0
  6. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  7. package/dist_ts/00_commitinfo_data.js +9 -0
  8. package/dist_ts/device/device.classes.device.d.ts +205 -0
  9. package/dist_ts/device/device.classes.device.js +344 -0
  10. package/dist_ts/devicemanager.classes.devicemanager.d.ts +210 -0
  11. package/dist_ts/devicemanager.classes.devicemanager.js +835 -0
  12. package/dist_ts/discovery/discovery.classes.homeassistant.d.ts +101 -0
  13. package/dist_ts/discovery/discovery.classes.homeassistant.js +313 -0
  14. package/dist_ts/discovery/discovery.classes.mdns.d.ts +90 -0
  15. package/dist_ts/discovery/discovery.classes.mdns.js +291 -0
  16. package/dist_ts/discovery/discovery.classes.networkscanner.d.ts +64 -0
  17. package/dist_ts/discovery/discovery.classes.networkscanner.js +535 -0
  18. package/dist_ts/discovery/discovery.classes.ssdp.d.ts +112 -0
  19. package/dist_ts/discovery/discovery.classes.ssdp.js +255 -0
  20. package/dist_ts/factories/index.d.ts +190 -0
  21. package/dist_ts/factories/index.js +350 -0
  22. package/dist_ts/features/feature.abstract.d.ts +118 -0
  23. package/dist_ts/features/feature.abstract.js +172 -0
  24. package/dist_ts/features/feature.camera.d.ts +95 -0
  25. package/dist_ts/features/feature.camera.js +162 -0
  26. package/dist_ts/features/feature.climate.d.ts +174 -0
  27. package/dist_ts/features/feature.climate.js +328 -0
  28. package/dist_ts/features/feature.cover.d.ts +121 -0
  29. package/dist_ts/features/feature.cover.js +217 -0
  30. package/dist_ts/features/feature.fan.d.ts +127 -0
  31. package/dist_ts/features/feature.fan.js +235 -0
  32. package/dist_ts/features/feature.light.d.ts +156 -0
  33. package/dist_ts/features/feature.light.js +296 -0
  34. package/dist_ts/features/feature.lock.d.ts +103 -0
  35. package/dist_ts/features/feature.lock.js +170 -0
  36. package/dist_ts/features/feature.playback.d.ts +98 -0
  37. package/dist_ts/features/feature.playback.js +191 -0
  38. package/dist_ts/features/feature.power.d.ts +79 -0
  39. package/dist_ts/features/feature.power.js +173 -0
  40. package/dist_ts/features/feature.print.d.ts +78 -0
  41. package/dist_ts/features/feature.print.js +226 -0
  42. package/dist_ts/features/feature.scan.d.ts +79 -0
  43. package/dist_ts/features/feature.scan.js +315 -0
  44. package/dist_ts/features/feature.sensor.d.ts +95 -0
  45. package/dist_ts/features/feature.sensor.js +147 -0
  46. package/dist_ts/features/feature.snmp.d.ts +88 -0
  47. package/dist_ts/features/feature.snmp.js +175 -0
  48. package/dist_ts/features/feature.switch.d.ts +75 -0
  49. package/dist_ts/features/feature.switch.js +126 -0
  50. package/dist_ts/features/feature.volume.d.ts +104 -0
  51. package/dist_ts/features/feature.volume.js +186 -0
  52. package/dist_ts/features/index.d.ts +19 -0
  53. package/dist_ts/features/index.js +23 -0
  54. package/dist_ts/helpers/helpers.iprange.d.ts +35 -0
  55. package/dist_ts/helpers/helpers.iprange.js +151 -0
  56. package/dist_ts/helpers/helpers.retry.d.ts +25 -0
  57. package/dist_ts/helpers/helpers.retry.js +69 -0
  58. package/dist_ts/index.d.ts +15 -0
  59. package/dist_ts/index.js +40 -0
  60. package/dist_ts/interfaces/feature.interfaces.d.ts +255 -0
  61. package/dist_ts/interfaces/feature.interfaces.js +6 -0
  62. package/dist_ts/interfaces/homeassistant.interfaces.d.ts +550 -0
  63. package/dist_ts/interfaces/homeassistant.interfaces.js +111 -0
  64. package/dist_ts/interfaces/index.d.ts +330 -0
  65. package/dist_ts/interfaces/index.js +16 -0
  66. package/dist_ts/interfaces/smarthome.interfaces.d.ts +282 -0
  67. package/dist_ts/interfaces/smarthome.interfaces.js +7 -0
  68. package/dist_ts/paths.d.ts +1 -0
  69. package/dist_ts/paths.js +3 -0
  70. package/dist_ts/plugins.d.ts +23 -0
  71. package/dist_ts/plugins.js +30 -0
  72. package/dist_ts/protocols/index.d.ts +12 -0
  73. package/dist_ts/protocols/index.js +21 -0
  74. package/dist_ts/protocols/protocol.escl.d.ts +58 -0
  75. package/dist_ts/protocols/protocol.escl.js +364 -0
  76. package/dist_ts/protocols/protocol.homeassistant.d.ts +248 -0
  77. package/dist_ts/protocols/protocol.homeassistant.js +611 -0
  78. package/dist_ts/protocols/protocol.ipp.d.ts +45 -0
  79. package/dist_ts/protocols/protocol.ipp.js +284 -0
  80. package/dist_ts/protocols/protocol.nut.d.ts +160 -0
  81. package/dist_ts/protocols/protocol.nut.js +364 -0
  82. package/dist_ts/protocols/protocol.sane.d.ts +104 -0
  83. package/dist_ts/protocols/protocol.sane.js +597 -0
  84. package/dist_ts/protocols/protocol.snmp.d.ts +133 -0
  85. package/dist_ts/protocols/protocol.snmp.js +333 -0
  86. package/dist_ts/protocols/protocol.upnp.d.ts +207 -0
  87. package/dist_ts/protocols/protocol.upnp.js +442 -0
  88. package/dist_ts/protocols/protocol.upssnmp.d.ts +160 -0
  89. package/dist_ts/protocols/protocol.upssnmp.js +263 -0
  90. package/npmextra.json +24 -0
  91. package/package.json +39 -0
  92. package/readme.hints.md +115 -0
  93. package/readme.md +624 -0
  94. package/test/test.ts +261 -0
  95. package/ts/00_commitinfo_data.ts +8 -0
  96. package/ts/device/device.classes.device.ts +454 -0
  97. package/ts/devicemanager.classes.devicemanager.ts +1219 -0
  98. package/ts/discovery/discovery.classes.homeassistant.ts +376 -0
  99. package/ts/discovery/discovery.classes.mdns.ts +347 -0
  100. package/ts/discovery/discovery.classes.networkscanner.ts +648 -0
  101. package/ts/discovery/discovery.classes.ssdp.ts +357 -0
  102. package/ts/factories/index.ts +696 -0
  103. package/ts/features/feature.abstract.ts +251 -0
  104. package/ts/features/feature.camera.ts +214 -0
  105. package/ts/features/feature.climate.ts +407 -0
  106. package/ts/features/feature.cover.ts +278 -0
  107. package/ts/features/feature.fan.ts +296 -0
  108. package/ts/features/feature.light.ts +369 -0
  109. package/ts/features/feature.lock.ts +223 -0
  110. package/ts/features/feature.playback.ts +246 -0
  111. package/ts/features/feature.power.ts +231 -0
  112. package/ts/features/feature.print.ts +297 -0
  113. package/ts/features/feature.scan.ts +370 -0
  114. package/ts/features/feature.sensor.ts +202 -0
  115. package/ts/features/feature.snmp.ts +235 -0
  116. package/ts/features/feature.switch.ts +170 -0
  117. package/ts/features/feature.volume.ts +256 -0
  118. package/ts/features/index.ts +25 -0
  119. package/ts/helpers/helpers.iprange.ts +172 -0
  120. package/ts/helpers/helpers.retry.ts +100 -0
  121. package/ts/index.ts +165 -0
  122. package/ts/interfaces/feature.interfaces.ts +359 -0
  123. package/ts/interfaces/homeassistant.interfaces.ts +666 -0
  124. package/ts/interfaces/index.ts +421 -0
  125. package/ts/interfaces/smarthome.interfaces.ts +421 -0
  126. package/ts/paths.ts +5 -0
  127. package/ts/plugins.ts +41 -0
  128. package/ts/protocols/index.ts +59 -0
  129. package/ts/protocols/protocol.escl.ts +439 -0
  130. package/ts/protocols/protocol.homeassistant.ts +737 -0
  131. package/ts/protocols/protocol.ipp.ts +329 -0
  132. package/ts/protocols/protocol.nut.ts +471 -0
  133. package/ts/protocols/protocol.sane.ts +694 -0
  134. package/ts/protocols/protocol.snmp.ts +439 -0
  135. package/ts/protocols/protocol.upnp.ts +627 -0
  136. package/ts/protocols/protocol.upssnmp.ts +377 -0
  137. package/tsconfig.json +12 -0
@@ -0,0 +1,454 @@
1
+ /**
2
+ * Universal Device class
3
+ * A device is a network endpoint that can have multiple features (capabilities)
4
+ */
5
+
6
+ import * as plugins from '../plugins.js';
7
+ import type {
8
+ TFeatureType,
9
+ IFeatureInfo,
10
+ IDiscoveredFeature,
11
+ } from '../interfaces/feature.interfaces.js';
12
+ import type { TDeviceStatus, IRetryOptions } from '../interfaces/index.js';
13
+ import { Feature, type TDeviceReference } from '../features/feature.abstract.js';
14
+
15
+ // ============================================================================
16
+ // Device Info Interface
17
+ // ============================================================================
18
+
19
+ /**
20
+ * Serializable device information
21
+ */
22
+ export interface IUniversalDeviceInfo {
23
+ id: string;
24
+ name: string;
25
+ address: string;
26
+ port: number;
27
+ status: TDeviceStatus;
28
+ manufacturer?: string;
29
+ model?: string;
30
+ serialNumber?: string;
31
+ firmwareVersion?: string;
32
+ features: IFeatureInfo[];
33
+ featureTypes: TFeatureType[];
34
+ }
35
+
36
+ /**
37
+ * Options for creating a device
38
+ */
39
+ export interface IDeviceCreateOptions {
40
+ name?: string;
41
+ manufacturer?: string;
42
+ model?: string;
43
+ serialNumber?: string;
44
+ firmwareVersion?: string;
45
+ retryOptions?: IRetryOptions;
46
+ }
47
+
48
+ // ============================================================================
49
+ // Universal Device Class
50
+ // ============================================================================
51
+
52
+ /**
53
+ * Universal Device - represents any network device with composable features
54
+ *
55
+ * Instead of having separate Scanner, Printer, Speaker classes, a Device can have
56
+ * any combination of features (scan, print, playback, volume, etc.)
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * // A multifunction printer/scanner
61
+ * const mfp = new UniversalDevice('192.168.1.100', 80);
62
+ * mfp.addFeature(new ScanFeature(mfp, 80));
63
+ * mfp.addFeature(new PrintFeature(mfp, 631));
64
+ *
65
+ * // Check capabilities
66
+ * if (mfp.hasFeature('scan') && mfp.hasFeature('print')) {
67
+ * const scanFeature = mfp.getFeature<ScanFeature>('scan');
68
+ * await scanFeature.scan({ format: 'pdf' });
69
+ * }
70
+ * ```
71
+ */
72
+ export class UniversalDevice extends plugins.events.EventEmitter {
73
+ // ============================================================================
74
+ // Properties
75
+ // ============================================================================
76
+
77
+ /**
78
+ * Unique device identifier
79
+ * Format: {address}:{port} or custom ID from discovery
80
+ */
81
+ public readonly id: string;
82
+
83
+ /**
84
+ * Human-readable device name
85
+ */
86
+ public name: string;
87
+
88
+ /**
89
+ * Device network address (IP or hostname)
90
+ */
91
+ public readonly address: string;
92
+
93
+ /**
94
+ * Primary device port
95
+ */
96
+ public readonly port: number;
97
+
98
+ /**
99
+ * Device manufacturer
100
+ */
101
+ public manufacturer?: string;
102
+
103
+ /**
104
+ * Device model
105
+ */
106
+ public model?: string;
107
+
108
+ /**
109
+ * Device serial number
110
+ */
111
+ public serialNumber?: string;
112
+
113
+ /**
114
+ * Device firmware version
115
+ */
116
+ public firmwareVersion?: string;
117
+
118
+ /**
119
+ * Current device status
120
+ */
121
+ protected _status: TDeviceStatus = 'unknown';
122
+
123
+ /**
124
+ * Map of features by type
125
+ */
126
+ protected _features: Map<TFeatureType, Feature> = new Map();
127
+
128
+ /**
129
+ * Retry options for features
130
+ */
131
+ protected _retryOptions: IRetryOptions;
132
+
133
+ // ============================================================================
134
+ // Constructor
135
+ // ============================================================================
136
+
137
+ constructor(
138
+ address: string,
139
+ port: number,
140
+ options?: IDeviceCreateOptions
141
+ ) {
142
+ super();
143
+ this.address = address;
144
+ this.port = port;
145
+ this.id = `${address}:${port}`;
146
+ this.name = options?.name ?? `Device at ${address}`;
147
+ this.manufacturer = options?.manufacturer;
148
+ this.model = options?.model;
149
+ this.serialNumber = options?.serialNumber;
150
+ this.firmwareVersion = options?.firmwareVersion;
151
+ this._retryOptions = options?.retryOptions ?? {};
152
+ }
153
+
154
+ // ============================================================================
155
+ // Status Management
156
+ // ============================================================================
157
+
158
+ /**
159
+ * Get current device status
160
+ */
161
+ public get status(): TDeviceStatus {
162
+ return this._status;
163
+ }
164
+
165
+ /**
166
+ * Set device status
167
+ */
168
+ public set status(value: TDeviceStatus) {
169
+ if (this._status !== value) {
170
+ const oldStatus = this._status;
171
+ this._status = value;
172
+ this.emit('status:changed', { oldStatus, newStatus: value });
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Check if device is online
178
+ */
179
+ public get isOnline(): boolean {
180
+ return this._status === 'online';
181
+ }
182
+
183
+ // ============================================================================
184
+ // Feature Management
185
+ // ============================================================================
186
+
187
+ /**
188
+ * Add a feature to the device
189
+ */
190
+ public addFeature(feature: Feature): void {
191
+ if (this._features.has(feature.type)) {
192
+ throw new Error(`Feature '${feature.type}' already exists on device ${this.id}`);
193
+ }
194
+
195
+ this._features.set(feature.type, feature);
196
+
197
+ // Forward feature events
198
+ feature.on('connected', () => this.emit('feature:connected', feature.type));
199
+ feature.on('disconnected', () => this.emit('feature:disconnected', feature.type));
200
+ feature.on('error', (error) => this.emit('feature:error', { type: feature.type, error }));
201
+ feature.on('state:changed', (states) => this.emit('feature:state:changed', { type: feature.type, ...states }));
202
+
203
+ this.emit('feature:added', feature);
204
+ }
205
+
206
+ /**
207
+ * Remove a feature from the device
208
+ */
209
+ public async removeFeature(type: TFeatureType): Promise<boolean> {
210
+ const feature = this._features.get(type);
211
+ if (!feature) {
212
+ return false;
213
+ }
214
+
215
+ // Disconnect the feature first
216
+ try {
217
+ await feature.disconnect();
218
+ } catch {
219
+ // Ignore disconnect errors
220
+ }
221
+
222
+ this._features.delete(type);
223
+ this.emit('feature:removed', type);
224
+ return true;
225
+ }
226
+
227
+ /**
228
+ * Check if device has a specific feature
229
+ */
230
+ public hasFeature(type: TFeatureType): boolean {
231
+ return this._features.has(type);
232
+ }
233
+
234
+ /**
235
+ * Check if device has all specified features
236
+ */
237
+ public hasFeatures(types: TFeatureType[]): boolean {
238
+ return types.every((type) => this._features.has(type));
239
+ }
240
+
241
+ /**
242
+ * Check if device has any of the specified features
243
+ */
244
+ public hasAnyFeature(types: TFeatureType[]): boolean {
245
+ return types.some((type) => this._features.has(type));
246
+ }
247
+
248
+ /**
249
+ * Get a feature by type (returns undefined if not available)
250
+ */
251
+ public getFeature<T extends Feature>(type: TFeatureType): T | undefined {
252
+ return this._features.get(type) as T | undefined;
253
+ }
254
+
255
+ /**
256
+ * Select a feature by type (throws if not available).
257
+ * Use this when you expect the device to have this feature and want fail-fast behavior.
258
+ *
259
+ * @param type The feature type to select
260
+ * @returns The feature instance
261
+ * @throws Error if the device does not have this feature
262
+ *
263
+ * @example
264
+ * ```typescript
265
+ * const scanFeature = device.selectFeature<ScanFeature>('scan');
266
+ * await scanFeature.connect();
267
+ * const result = await scanFeature.scan({ source: 'flatbed' });
268
+ * ```
269
+ */
270
+ public selectFeature<T extends Feature>(type: TFeatureType): T {
271
+ const feature = this._features.get(type) as T | undefined;
272
+ if (!feature) {
273
+ throw new Error(`Device '${this.name}' does not have feature '${type}'`);
274
+ }
275
+ return feature;
276
+ }
277
+
278
+ /**
279
+ * Get all features
280
+ */
281
+ public getFeatures(): Feature[] {
282
+ return Array.from(this._features.values());
283
+ }
284
+
285
+ /**
286
+ * Get all feature types
287
+ */
288
+ public getFeatureTypes(): TFeatureType[] {
289
+ return Array.from(this._features.keys());
290
+ }
291
+
292
+ /**
293
+ * Get feature count
294
+ */
295
+ public get featureCount(): number {
296
+ return this._features.size;
297
+ }
298
+
299
+ // ============================================================================
300
+ // Connection Management
301
+ // ============================================================================
302
+
303
+ /**
304
+ * Connect all features
305
+ */
306
+ public async connect(): Promise<void> {
307
+ const features = this.getFeatures();
308
+ const results = await Promise.allSettled(
309
+ features.map((f) => f.connect())
310
+ );
311
+
312
+ // Check if any connected successfully
313
+ const anyConnected = results.some((r) => r.status === 'fulfilled');
314
+
315
+ if (anyConnected) {
316
+ this.status = 'online';
317
+ } else if (results.length > 0) {
318
+ // All failed
319
+ this.status = 'error';
320
+ const errors = results
321
+ .filter((r): r is PromiseRejectedResult => r.status === 'rejected')
322
+ .map((r) => r.reason);
323
+ throw new AggregateError(errors, 'Failed to connect any features');
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Connect a specific feature
329
+ */
330
+ public async connectFeature(type: TFeatureType): Promise<void> {
331
+ const feature = this._features.get(type);
332
+ if (!feature) {
333
+ throw new Error(`Feature '${type}' not found on device ${this.id}`);
334
+ }
335
+ await feature.connect();
336
+
337
+ // Update device status if this is the first connected feature
338
+ if (this._status !== 'online') {
339
+ this.status = 'online';
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Disconnect all features
345
+ */
346
+ public async disconnect(): Promise<void> {
347
+ const features = this.getFeatures();
348
+ await Promise.allSettled(
349
+ features.map((f) => f.disconnect())
350
+ );
351
+ this.status = 'offline';
352
+ }
353
+
354
+ /**
355
+ * Disconnect a specific feature
356
+ */
357
+ public async disconnectFeature(type: TFeatureType): Promise<void> {
358
+ const feature = this._features.get(type);
359
+ if (feature) {
360
+ await feature.disconnect();
361
+ }
362
+
363
+ // Update device status if all features are disconnected
364
+ const anyConnected = this.getFeatures().some((f) => f.isConnected);
365
+ if (!anyConnected) {
366
+ this.status = 'offline';
367
+ }
368
+ }
369
+
370
+ // ============================================================================
371
+ // Serialization
372
+ // ============================================================================
373
+
374
+ /**
375
+ * Get device reference for features
376
+ */
377
+ public getDeviceReference(): TDeviceReference {
378
+ return {
379
+ id: this.id,
380
+ name: this.name,
381
+ address: this.address,
382
+ port: this.port,
383
+ };
384
+ }
385
+
386
+ /**
387
+ * Get serializable device info
388
+ */
389
+ public getDeviceInfo(): IUniversalDeviceInfo {
390
+ return {
391
+ id: this.id,
392
+ name: this.name,
393
+ address: this.address,
394
+ port: this.port,
395
+ status: this._status,
396
+ manufacturer: this.manufacturer,
397
+ model: this.model,
398
+ serialNumber: this.serialNumber,
399
+ firmwareVersion: this.firmwareVersion,
400
+ features: this.getFeatures().map((f) => f.getFeatureInfo()),
401
+ featureTypes: this.getFeatureTypes(),
402
+ };
403
+ }
404
+
405
+ /**
406
+ * Get retry options for features
407
+ */
408
+ public get retryOptions(): IRetryOptions {
409
+ return this._retryOptions;
410
+ }
411
+
412
+ // ============================================================================
413
+ // Static Factory Methods
414
+ // ============================================================================
415
+
416
+ /**
417
+ * Create a device from discovered features
418
+ */
419
+ public static fromDiscovery(
420
+ id: string,
421
+ name: string,
422
+ address: string,
423
+ port: number,
424
+ discoveredFeatures: IDiscoveredFeature[],
425
+ options?: IDeviceCreateOptions
426
+ ): UniversalDevice {
427
+ const device = new UniversalDevice(address, port, {
428
+ ...options,
429
+ name,
430
+ });
431
+
432
+ // Override the generated ID with the discovery ID
433
+ (device as { id: string }).id = id;
434
+
435
+ // Features will be added by the DeviceManager after creation
436
+ // This is because feature instances need protocol-specific initialization
437
+
438
+ return device;
439
+ }
440
+ }
441
+
442
+ // ============================================================================
443
+ // Type Guards
444
+ // ============================================================================
445
+
446
+ /**
447
+ * Check if a device has a specific feature (type guard)
448
+ */
449
+ export function deviceHasFeature<T extends Feature>(
450
+ device: UniversalDevice,
451
+ type: TFeatureType
452
+ ): device is UniversalDevice & { getFeature(type: TFeatureType): T } {
453
+ return device.hasFeature(type);
454
+ }