@ecobridge.xyz/devicemanager 3.1.0 → 3.2.0

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 (78) hide show
  1. package/.gitea/workflows/default_tags.yaml +0 -21
  2. package/.smartconfig.json +69 -0
  3. package/changelog.md +38 -1
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/device/device.classes.device.d.ts +2 -2
  6. package/dist_ts/device/device.classes.device.js +3 -3
  7. package/dist_ts/devicemanager.classes.devicemanager.d.ts +17 -4
  8. package/dist_ts/devicemanager.classes.devicemanager.js +99 -11
  9. package/dist_ts/discovery/discovery.classes.mdns.js +46 -11
  10. package/dist_ts/discovery/discovery.classes.networkscanner.d.ts +5 -1
  11. package/dist_ts/discovery/discovery.classes.networkscanner.js +44 -3
  12. package/dist_ts/discovery/discovery.classes.ssdp.d.ts +52 -21
  13. package/dist_ts/discovery/discovery.classes.ssdp.js +407 -117
  14. package/dist_ts/events/events.observable.d.ts +15 -0
  15. package/dist_ts/events/events.observable.js +22 -0
  16. package/dist_ts/events/index.d.ts +1 -0
  17. package/dist_ts/events/index.js +2 -0
  18. package/dist_ts/factories/index.js +7 -5
  19. package/dist_ts/features/feature.abstract.d.ts +2 -2
  20. package/dist_ts/features/feature.abstract.js +3 -3
  21. package/dist_ts/features/feature.print.d.ts +3 -0
  22. package/dist_ts/features/feature.print.js +17 -2
  23. package/dist_ts/features/feature.scan.d.ts +5 -2
  24. package/dist_ts/features/feature.scan.js +98 -27
  25. package/dist_ts/index.d.ts +4 -2
  26. package/dist_ts/index.js +4 -2
  27. package/dist_ts/interfaces/feature.interfaces.d.ts +22 -2
  28. package/dist_ts/interfaces/index.d.ts +22 -4
  29. package/dist_ts/interfaces/index.js +1 -1
  30. package/dist_ts/plugins.d.ts +6 -15
  31. package/dist_ts/plugins.js +7 -19
  32. package/dist_ts/protocols/index.d.ts +2 -1
  33. package/dist_ts/protocols/index.js +4 -2
  34. package/dist_ts/protocols/protocol.brother.d.ts +67 -0
  35. package/dist_ts/protocols/protocol.brother.js +560 -0
  36. package/dist_ts/protocols/protocol.escl.d.ts +13 -4
  37. package/dist_ts/protocols/protocol.escl.js +345 -178
  38. package/dist_ts/protocols/protocol.ipp.d.ts +2 -0
  39. package/dist_ts/protocols/protocol.ipp.js +22 -2
  40. package/dist_ts/providers/index.d.ts +1 -0
  41. package/dist_ts/providers/index.js +2 -0
  42. package/dist_ts/providers/provider.smb.d.ts +60 -0
  43. package/dist_ts/providers/provider.smb.js +224 -0
  44. package/license.md +21 -0
  45. package/package.json +15 -20
  46. package/pnpm-workspace.yaml +6 -0
  47. package/readme.md +157 -78
  48. package/test/test.brother.node.ts +306 -0
  49. package/test/test.escl.node.ts +233 -0
  50. package/test/test.ipp.node.ts +75 -0
  51. package/test/{test.ts → test.node.ts} +37 -45
  52. package/test/test.smb.node.ts +123 -0
  53. package/test/test.ssdp.node.ts +978 -0
  54. package/ts/00_commitinfo_data.ts +1 -1
  55. package/ts/device/device.classes.device.ts +2 -2
  56. package/ts/devicemanager.classes.devicemanager.ts +125 -12
  57. package/ts/discovery/discovery.classes.mdns.ts +40 -11
  58. package/ts/discovery/discovery.classes.networkscanner.ts +51 -2
  59. package/ts/discovery/discovery.classes.ssdp.ts +493 -130
  60. package/ts/events/events.observable.ts +31 -0
  61. package/ts/events/index.ts +4 -0
  62. package/ts/factories/index.ts +7 -5
  63. package/ts/features/feature.abstract.ts +2 -2
  64. package/ts/features/feature.print.ts +16 -1
  65. package/ts/features/feature.scan.ts +93 -27
  66. package/ts/index.ts +21 -0
  67. package/ts/interfaces/feature.interfaces.ts +23 -2
  68. package/ts/interfaces/index.ts +23 -4
  69. package/ts/plugins.ts +6 -25
  70. package/ts/protocols/index.ts +11 -1
  71. package/ts/protocols/protocol.brother.ts +648 -0
  72. package/ts/protocols/protocol.escl.ts +348 -191
  73. package/ts/protocols/protocol.ipp.ts +24 -1
  74. package/ts/providers/index.ts +7 -0
  75. package/ts/providers/provider.smb.ts +326 -0
  76. package/dist_ts/protocols/protocol.ipp.old.d.ts +0 -45
  77. package/dist_ts/protocols/protocol.ipp.old.js +0 -284
  78. package/npmextra.json +0 -24
@@ -32,6 +32,7 @@ tap.test('should export device factories', async () => {
32
32
 
33
33
  tap.test('should export protocol implementations', async () => {
34
34
  expect(devicemanager.EsclProtocol).toBeDefined();
35
+ expect(devicemanager.BrotherScanProtocol).toBeDefined();
35
36
  expect(devicemanager.SaneProtocol).toBeDefined();
36
37
  expect(devicemanager.IppProtocol).toBeDefined();
37
38
  expect(devicemanager.SnmpProtocol).toBeDefined();
@@ -127,50 +128,6 @@ tap.test('withRetry should throw after max retries', async () => {
127
128
  expect(callCount).toEqual(3); // Initial + 2 retries
128
129
  });
129
130
 
130
- // Test discovery (non-blocking)
131
- tap.test('should start and stop discovery', async () => {
132
- const dm = new devicemanager.DeviceManager();
133
-
134
- // Track events
135
- let discoveryStarted = false;
136
- let discoveryStopped = false;
137
-
138
- dm.on('discovery:started', () => {
139
- discoveryStarted = true;
140
- });
141
-
142
- dm.on('discovery:stopped', () => {
143
- discoveryStopped = true;
144
- });
145
-
146
- await dm.startDiscovery();
147
- expect(dm.isDiscovering).toEqual(true);
148
- expect(discoveryStarted).toEqual(true);
149
-
150
- // Wait a bit for potential device discovery
151
- await new Promise((resolve) => setTimeout(resolve, 2000));
152
-
153
- // Log discovered devices using new API
154
- const scanners = dm.getDevices({ hasFeature: 'scan' });
155
- const printers = dm.getDevices({ hasFeature: 'print' });
156
- console.log(`Discovered ${scanners.length} scanner(s) and ${printers.length} printer(s)`);
157
-
158
- for (const scanner of scanners) {
159
- const scanFeature = scanner.getFeature<devicemanager.ScanFeature>('scan');
160
- console.log(` Scanner: ${scanner.name} (${scanner.address}) - ${scanFeature?.protocol || 'unknown'}`);
161
- }
162
-
163
- for (const printer of printers) {
164
- console.log(` Printer: ${printer.name} (${printer.address})`);
165
- }
166
-
167
- await dm.stopDiscovery();
168
- expect(dm.isDiscovering).toEqual(false);
169
- expect(discoveryStopped).toEqual(true);
170
-
171
- await dm.shutdown();
172
- });
173
-
174
131
  // Test Scanner creation using factory
175
132
  tap.test('should create Scanner device using factory', async () => {
176
133
  const scanner = devicemanager.createScanner({
@@ -197,6 +154,22 @@ tap.test('should create Scanner device using factory', async () => {
197
154
  expect(scanFeature.protocol).toEqual('escl');
198
155
  });
199
156
 
157
+ tap.test('duplex-capable scanners default to duplex ADF', async () => {
158
+ const scanner = devicemanager.createScanner({
159
+ id: 'test:brother-scanner:1',
160
+ name: 'Brother Scanner',
161
+ address: '192.168.1.101',
162
+ port: 54921,
163
+ protocol: 'brother',
164
+ txtRecords: {},
165
+ });
166
+ const scanFeature = scanner.selectFeature<devicemanager.ScanFeature>('scan');
167
+ const defaults = (scanFeature as unknown as {
168
+ resolveOptions(options?: devicemanager.IScanOptions): Required<devicemanager.IScanOptions>;
169
+ }).resolveOptions();
170
+ expect(defaults.source).toEqual('adf-duplex');
171
+ });
172
+
200
173
  // Test Printer creation using factory
201
174
  tap.test('should create Printer device using factory', async () => {
202
175
  const printer = devicemanager.createPrinter({
@@ -204,8 +177,8 @@ tap.test('should create Printer device using factory', async () => {
204
177
  name: 'Test Printer',
205
178
  address: '192.168.1.101',
206
179
  port: 631,
207
- ippPath: '/ipp/print',
208
180
  txtRecords: {
181
+ 'rp': '/ipp/print',
209
182
  'ty': 'Brother HL-L2350DW',
210
183
  'Color': 'T',
211
184
  'Duplex': 'T',
@@ -221,6 +194,25 @@ tap.test('should create Printer device using factory', async () => {
221
194
  expect(printFeature).toBeInstanceOf(devicemanager.PrintFeature);
222
195
  });
223
196
 
197
+ tap.test('should create a Sonos speaker without an external Sonos client', async () => {
198
+ const speaker = devicemanager.createSpeaker({
199
+ id: 'test:speaker:sonos',
200
+ name: 'Living Room',
201
+ address: '192.168.1.102',
202
+ port: 1400,
203
+ protocol: 'sonos',
204
+ });
205
+
206
+ const playback = speaker.selectFeature<devicemanager.PlaybackFeature>('playback');
207
+ const volume = speaker.selectFeature<devicemanager.VolumeFeature>('volume');
208
+
209
+ expect(speaker.id).toEqual('test:speaker:sonos');
210
+ expect(playback.protocol).toEqual('sonos');
211
+ expect(playback.supportsQueue).toEqual(true);
212
+ expect(playback.supportsSeek).toEqual(true);
213
+ expect(volume.protocol).toEqual('sonos');
214
+ });
215
+
224
216
  // Test UniversalDevice feature management
225
217
  tap.test('should manage features on UniversalDevice', async () => {
226
218
  const device = new devicemanager.UniversalDevice('192.168.1.50', 80, {
@@ -0,0 +1,123 @@
1
+ import { tap, expect } from '@git.zone/tstest/tapbundle';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { filter, firstValueFrom, take, timeout, toArray } from 'rxjs';
6
+ import {
7
+ DeviceManager,
8
+ type ISmbServerAdapter,
9
+ type ISmbShareProviderOptions,
10
+ } from '../ts/index.js';
11
+
12
+ class FakeSmbServer implements ISmbServerAdapter {
13
+ public running = false;
14
+
15
+ constructor(private readonly options: ISmbShareProviderOptions) {}
16
+
17
+ public async start() {
18
+ this.running = true;
19
+ return {
20
+ host: this.options.host ?? '127.0.0.1',
21
+ port: this.options.port ?? 445,
22
+ address: `smb://${this.options.host ?? '127.0.0.1'}:${this.options.port ?? 445}`,
23
+ shares: this.options.shares.map((share) => share.name),
24
+ };
25
+ }
26
+
27
+ public async stop(): Promise<void> {
28
+ this.running = false;
29
+ }
30
+
31
+ public async status() {
32
+ return {
33
+ running: this.running,
34
+ host: this.options.host,
35
+ port: this.options.port,
36
+ shares: this.options.shares.map((share) => share.name),
37
+ };
38
+ }
39
+
40
+ public getConnectionOptions() {
41
+ return {
42
+ host: this.options.host ?? '127.0.0.1',
43
+ port: this.options.port ?? 445,
44
+ };
45
+ }
46
+ }
47
+
48
+ tap.test('SMB provider emits settled documents through provider and manager RxJS streams', async () => {
49
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'devicemanager-smb-'));
50
+ const sharePath = path.join(tempRoot, 'scans');
51
+ const manager = new DeviceManager({ autoDiscovery: false });
52
+ const options: ISmbShareProviderOptions = {
53
+ host: '127.0.0.1',
54
+ port: 1445,
55
+ shares: [{ name: 'brother-scans', path: sharePath }],
56
+ pollIntervalMs: 20,
57
+ settleTimeMs: 60,
58
+ };
59
+
60
+ const provider = await manager.provideSmbShares(options, {
61
+ serverFactory: (serverOptions) => new FakeSmbServer({ ...options, ...serverOptions }),
62
+ });
63
+
64
+ try {
65
+ const providerEventPromise = firstValueFrom(provider.documents$.pipe(timeout({ first: 3000 })));
66
+ const managerEventPromise = firstValueFrom(manager.events$.pipe(
67
+ filter((event) => event.name === 'smb:document:arrived'),
68
+ timeout({ first: 3000 })
69
+ ));
70
+
71
+ const documentPath = path.join(sharePath, 'nested', 'scan.pdf');
72
+ await fs.mkdir(path.dirname(documentPath), { recursive: true });
73
+ await fs.writeFile(documentPath, 'first');
74
+ await new Promise((resolve) => setTimeout(resolve, 35));
75
+ await fs.appendFile(documentPath, '-complete');
76
+
77
+ const providerEvent = await providerEventPromise;
78
+ const managerEvent = await managerEventPromise;
79
+ expect(providerEvent.shareName).toEqual('brother-scans');
80
+ expect(providerEvent.relativePath).toEqual('nested/scan.pdf');
81
+ expect(providerEvent.size).toEqual('first-complete'.length);
82
+ expect((managerEvent.args[0] as { absolutePath: string }).absolutePath).toEqual(documentPath);
83
+ } finally {
84
+ await manager.stopSmbShareProvider(provider);
85
+ await fs.rm(tempRoot, { recursive: true, force: true });
86
+ }
87
+ });
88
+
89
+ tap.test('SMB provider keeps document events distinct for shares with the same path', async () => {
90
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'devicemanager-smb-shares-'));
91
+ const manager = new DeviceManager({ autoDiscovery: false });
92
+ const options: ISmbShareProviderOptions = {
93
+ host: '127.0.0.1',
94
+ port: 1445,
95
+ shares: [
96
+ { name: 'scanner-a', path: tempRoot },
97
+ { name: 'scanner-b', path: tempRoot },
98
+ ],
99
+ pollIntervalMs: 20,
100
+ settleTimeMs: 40,
101
+ };
102
+
103
+ const provider = await manager.provideSmbShares(options, {
104
+ serverFactory: (serverOptions) => new FakeSmbServer({ ...options, ...serverOptions }),
105
+ });
106
+
107
+ try {
108
+ const eventsPromise = firstValueFrom(provider.documents$.pipe(
109
+ take(2),
110
+ toArray(),
111
+ timeout({ first: 3000 })
112
+ ));
113
+
114
+ await fs.writeFile(path.join(tempRoot, 'scan.pdf'), 'complete');
115
+ const events = await eventsPromise;
116
+ expect(events.map((event) => event.shareName).sort()).toEqual(['scanner-a', 'scanner-b']);
117
+ } finally {
118
+ await manager.stopSmbShareProvider(provider);
119
+ await fs.rm(tempRoot, { recursive: true, force: true });
120
+ }
121
+ });
122
+
123
+ export default tap.start();