@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
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@ecobridge.xyz/devicemanager",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "private": false,
5
5
  "description": "a device manager for talking to devices on network and over usb",
6
6
  "main": "dist_ts/index.js",
7
7
  "typings": "dist_ts/index.d.ts",
8
8
  "type": "module",
9
+ "packageManager": "pnpm@11.21.0",
9
10
  "author": "Task Venture Capital GmbH",
10
11
  "license": "MIT",
11
12
  "scripts": {
@@ -14,26 +15,20 @@
14
15
  "buildDocs": "(tsdoc)"
15
16
  },
16
17
  "devDependencies": {
17
- "@git.zone/tsbuild": "^4.1.0",
18
- "@git.zone/tsrun": "^2.0.0",
19
- "@git.zone/tstest": "^3.1.3",
20
- "@types/node": "^25.0.3",
21
- "@types/ws": "^8.18.1"
18
+ "@git.zone/tsbuild": "4.4.2",
19
+ "@git.zone/tsrun": "2.0.6",
20
+ "@git.zone/tstest": "4.0.0",
21
+ "@types/node": "26.2.0",
22
+ "@types/ws": "8.18.1"
22
23
  },
23
24
  "dependencies": {
24
- "@push.rocks/smartdelay": "^3.0.5",
25
- "@push.rocks/smartevent": "^2.0.5",
26
- "@push.rocks/smartnetwork": "^4.4.0",
27
- "@push.rocks/smartpath": "^6.0.0",
28
- "@push.rocks/smartpromise": "^4.2.3",
29
- "@push.rocks/smartrequest": "^5.0.1",
30
- "@push.rocks/smartxml": "^2.0.0",
31
- "bonjour-service": "^1.3.0",
32
- "castv2-client": "^1.2.0",
33
- "ipp": "^2.0.1",
34
- "net-snmp": "^3.26.0",
35
- "node-ssdp": "^4.0.1",
36
- "sonos": "^1.14.2",
37
- "ws": "^8.19.0"
25
+ "@push.rocks/smartdelay": "3.1.0",
26
+ "@push.rocks/smartpath": "6.0.0",
27
+ "@push.rocks/smartsamba": "0.3.1",
28
+ "bonjour-service": "1.4.4",
29
+ "fast-xml-parser": "4.5.7",
30
+ "net-snmp": "3.26.3",
31
+ "rxjs": "7.8.2",
32
+ "ws": "8.21.1"
38
33
  }
39
34
  }
@@ -0,0 +1,6 @@
1
+ allowBuilds:
2
+ esbuild: true
3
+ mongodb-memory-server: false
4
+ puppeteer: false
5
+ minimumReleaseAgeExclude:
6
+ - '@push.rocks/smartsamba@0.3.1'
package/readme.md CHANGED
@@ -7,14 +7,15 @@ A comprehensive, TypeScript-first device manager for discovering and communicati
7
7
 
8
8
  ## 🎯 Overview
9
9
 
10
- `@ecobridge.xyz/devicemanager` provides a unified, object-oriented API for discovering and controlling network devices. Whether you're building a document scanning workflow, managing printers, controlling smart home devices, or monitoring UPS systems — this library has you covered.
10
+ `@ecobridge.xyz/devicemanager` provides a unified, object-oriented API for discovering and modeling network devices, with live communication where protocol implementations are available. Whether you're building a document scanning workflow, managing printers, controlling smart home devices, or monitoring UPS systems — this library has you covered.
11
11
 
12
12
  **Supported Device Types:**
13
- - 🖨️ **Scanners** — eSCL (AirScan), SANE protocols
14
- - 📄 **Printers** — IPP, JetDirect protocols
15
- - 🔊 **Speakers** — Sonos, AirPlay, Chromecast, DLNA
13
+ - 🖨️ **Scanners** — Brother native network scanning, eSCL (AirScan), and SANE
14
+ - 📄 **Printers** — IPP/AirPrint discovery, capabilities, jobs, cancellation, and print execution; JetDirect/raw port detection
15
+ - 📁 **SMB Shares** — Host local folders through smartsamba and emit settled document arrivals through RxJS
16
+ - 🔊 **Speakers** — Sonos, AirPlay, Chromecast, and DLNA discovery with normalized feature/factory modeling
16
17
  - 🔋 **UPS Systems** — NUT, SNMP protocols
17
- - 📡 **SNMP Devices** — Generic SNMP v1/v2c/v3 support
18
+ - 📡 **SNMP Devices** — Community-based SNMP v1 and v2c sessions; v3 is not implemented
18
19
  - 🏠 **Smart Home** — Home Assistant integration (lights, switches, sensors, climate, locks, fans, cameras, covers)
19
20
 
20
21
  ## Issue Reporting and Security
@@ -107,7 +108,7 @@ The library follows a clean, composable architecture:
107
108
 
108
109
  ┌─────────────────────────────────────────────────────────────┐
109
110
  │ Protocols │
110
- eSCL │ SANE │ IPP │ SNMP │ NUT │ UPnP/SOAP Home Assistant│
111
+ Brother Scan │ eSCL │ SANE │ IPP │ SNMP │ NUT │ UPnP/SOAP
111
112
  └─────────────────────────────────────────────────────────────┘
112
113
  ```
113
114
 
@@ -119,13 +120,14 @@ The central orchestrator for device discovery and management.
119
120
 
120
121
  ```typescript
121
122
  const manager = new DeviceManager({
122
- autoDiscovery: true, // Enable mDNS/SSDP auto-discovery
123
123
  discoveryTimeout: 10000, // Discovery timeout in ms
124
124
  enableRetry: true, // Enable retry with exponential backoff
125
125
  maxRetries: 5, // Maximum retry attempts
126
126
  });
127
127
  ```
128
128
 
129
+ Constructing `DeviceManager` does not initiate network discovery. Call `startDiscovery()` explicitly when continuous mDNS/SSDP discovery should begin; the `autoDiscovery` option does not currently trigger startup.
130
+
129
131
  #### Discovery Methods
130
132
 
131
133
  ```typescript
@@ -141,6 +143,7 @@ const printers = await manager.discoverPrinters('192.168.1.0/24');
141
143
  // General network scan (all device types)
142
144
  const devices = await manager.scanNetwork({
143
145
  ipRange: '192.168.1.0/24',
146
+ probeBrother: true,
144
147
  probeEscl: true,
145
148
  probeIpp: true,
146
149
  probeSane: true,
@@ -236,16 +239,46 @@ const result = await scanFeature.scan({
236
239
 
237
240
  // result.data is a Buffer containing the scanned image
238
241
  await fs.writeFile('scan.jpg', result.data);
242
+
243
+ // ADF scans expose every returned document. `data` remains the first document
244
+ // for backwards compatibility. Use `pages` to avoid dropping later sheets/sides.
245
+ console.log(`Scanner returned ${result.pageCount} document(s)`);
246
+ for (const [index, page] of (result.pages ?? []).entries()) {
247
+ await fs.writeFile(`scan-${index + 1}.jpg`, page.data);
248
+ }
239
249
  ```
240
250
 
251
+ If a scanner times out or cancels after transferring one or more documents,
252
+ `EsclScanError.partialResult` contains those documents with `isComplete: false`.
253
+ Callers can persist `partialResult.pages` before deciding whether to retry.
254
+ Some scanner firmware accepts eSCL settings but returns a different format or
255
+ resolution. Check `result.settingsWarnings` before treating the requested scan
256
+ quality as confirmed.
257
+
258
+ Brother devices discovered on TCP 54921 use the native scan transport before
259
+ eSCL. It explicitly selects `AUTO` for ADF input, controls simplex/duplex, and
260
+ rejects a lease when the device does not grant the requested resolution. The
261
+ native transport currently returns color JPEG pages; unsupported output formats
262
+ or color modes fail explicitly instead of being silently substituted.
263
+ Duplex-capable scanners default to `adf-duplex`; callers can still request
264
+ `adf` or `flatbed` explicitly. The transport preserves every returned side.
265
+ Blank-page classification and removal belong in consuming applications, where
266
+ document-specific review rules can be applied.
267
+
241
268
  #### 📄 PrintFeature
242
269
 
270
+ `PrintFeature.print()` submits jobs through IPP, the print transport used by
271
+ AirPrint. It reads printer capabilities, submits jobs, reports job state, and
272
+ supports cancellation. Network discovery can also identify an open
273
+ JetDirect/raw port at TCP 9100, but JetDirect print execution is not implemented.
274
+
243
275
  ```typescript
244
276
  const printFeature = device.selectFeature<PrintFeature>('print');
245
277
  await printFeature.connect();
246
278
 
247
279
  // Get printer capabilities
248
280
  const caps = await printFeature.getCapabilities();
281
+ console.log(caps.airPrintSupported, caps.documentFormats, caps.resolutions);
249
282
 
250
283
  // Print a document
251
284
  const job = await printFeature.print(pdfBuffer, {
@@ -257,31 +290,87 @@ const job = await printFeature.print(pdfBuffer, {
257
290
  jobName: 'My Document',
258
291
  });
259
292
 
260
- // Check job status
261
- const status = await printFeature.getJobStatus(job.id);
293
+ // Get current job information
294
+ const jobInfo = await printFeature.getJobInfo(job.id);
295
+ ```
296
+
297
+ #### 📁 SMB Share Provider
298
+
299
+ `DeviceManager.provideSmbShares()` hosts one or more local folders with
300
+ `@push.rocks/smartsamba`. The provider waits until a new file has stopped
301
+ changing before it emits `document:arrived`, which keeps consumers from opening
302
+ a scan while the sending device is still writing it.
303
+
304
+ ```typescript
305
+ import { DeviceManager } from '@ecobridge.xyz/devicemanager';
306
+
307
+ const manager = new DeviceManager({ autoDiscovery: false });
308
+ const provider = await manager.provideSmbShares({
309
+ host: '0.0.0.0',
310
+ port: 445,
311
+ users: [{
312
+ username: 'scanner',
313
+ password: process.env.SMB_SCANNER_PASSWORD!,
314
+ }],
315
+ shares: [{
316
+ name: 'scans',
317
+ path: '/srv/device-scans',
318
+ users: [{ username: 'scanner', access: 'readWrite' }],
319
+ }],
320
+ settleTimeMs: 1000,
321
+ });
322
+
323
+ const subscription = provider.documents$.subscribe((document) => {
324
+ console.log(`New document: ${document.shareName}/${document.relativePath}`);
325
+ });
326
+
327
+ // The same event is forwarded through the manager as smb:document:arrived.
328
+ const managerSubscription = manager.events$.subscribe((event) => {
329
+ if (event.name === 'smb:document:arrived') {
330
+ console.log(event.args[0]);
331
+ }
332
+ });
333
+
334
+ // Later:
335
+ subscription.unsubscribe();
336
+ managerSubscription.unsubscribe();
337
+ await manager.shutdown();
262
338
  ```
263
339
 
340
+ The provider recursively monitors every configured share. It also emits
341
+ `document:changed`, `document:removed`, `started`, `stopped`, and
342
+ `provider:error`. Existing files are treated as the startup baseline unless
343
+ `emitExistingDocuments` is enabled.
344
+
345
+ Binding SMB's standard TCP port 445 can require elevated privileges. Published
346
+ smartsamba 0.3.1 packages support signed SMB 2.0.2 and 2.1 clients and include
347
+ Linux amd64 and arm64 engines. Other platforms can provide a locally built engine
348
+ through `SMARTSAMBA_RUST_BINARY`.
349
+ Do not store SMB passwords in device metadata or log provider options.
350
+
264
351
  #### 🔊 PlaybackFeature & VolumeFeature
265
352
 
353
+ `PlaybackFeature` and `VolumeFeature` provide normalized capability and state models. No external Sonos or Chromecast protocol client is bundled or wired into these features, so their methods update local feature state and emit events but do not provide live playback control for those devices.
354
+
266
355
  ```typescript
267
356
  const playback = device.selectFeature<PlaybackFeature>('playback');
268
357
  const volume = device.selectFeature<VolumeFeature>('volume');
269
358
  await playback.connect();
270
359
 
271
- // Control playback
360
+ // Update normalized playback state
272
361
  await playback.play('http://example.com/audio.mp3');
273
362
  await playback.pause();
274
363
  await playback.stop();
275
364
  await playback.seek(120); // seconds
276
365
 
277
- // Get playback status
278
- const status = await playback.getStatus();
279
- // { state: 'playing', position: 45, duration: 180, track: { title: '...' } }
366
+ // Get normalized playback status
367
+ const status = await playback.getPlaybackStatus();
368
+ // { state: 'stopped', position: 120, duration: 0, track: undefined }
280
369
 
281
- // Control volume
370
+ // Update normalized volume state
282
371
  await volume.setVolume(50); // 0-100
283
- await volume.mute();
284
- await volume.unmute();
372
+ await volume.setMute(true);
373
+ await volume.toggleMute();
285
374
  const level = await volume.getVolume();
286
375
  ```
287
376
 
@@ -293,14 +382,16 @@ await power.connect();
293
382
 
294
383
  // Get UPS status
295
384
  const status = await power.getStatus();
296
- // { status: 'online', battery: { charge: 100, runtime: 1800, voltage: 13.8 }, ... }
385
+ // 'online' | 'onbattery' | 'lowbattery' | ... | 'unknown'
297
386
 
298
387
  // Get battery info
299
388
  const battery = await power.getBatteryInfo();
300
- // { charge: 95, runtime: 1500, health: 'good' }
389
+ // { charge: 0, runtime: 0 } until a protocol-specific implementation updates it
301
390
 
302
- // Run self-test
303
- await power.runTest();
391
+ // Run a battery test when supported
392
+ if (power.supportsTest) {
393
+ await power.testBattery();
394
+ }
304
395
  ```
305
396
 
306
397
  #### 🏠 Smart Home Features
@@ -309,9 +400,9 @@ await power.runTest();
309
400
  // Light control
310
401
  const light = device.selectFeature<LightFeature>('light');
311
402
  await light.turnOn();
312
- await light.setBrightness(80);
313
- await light.setColor({ r: 255, g: 100, b: 50 });
314
- await light.setColorTemperature(4000); // Kelvin
403
+ await light.setBrightness(80); // 0-255
404
+ await light.setRgbColor(255, 100, 50);
405
+ await light.setColorTemp(4000); // Kelvin
315
406
 
316
407
  // Switch control
317
408
  const switch_ = device.selectFeature<SwitchFeature>('switch');
@@ -321,13 +412,16 @@ await switch_.toggle();
321
412
 
322
413
  // Climate control
323
414
  const climate = device.selectFeature<ClimateFeature>('climate');
324
- await climate.setTargetTemperature(22);
325
- await climate.setMode('heat'); // 'heat' | 'cool' | 'auto' | 'off'
415
+ await climate.setTargetTemp(22);
416
+ await climate.setHvacMode('heat'); // 'heat' | 'cool' | 'auto' | 'off'
326
417
 
327
418
  // Sensor reading
328
419
  const sensor = device.selectFeature<SensorFeature>('sensor');
329
- const reading = await sensor.getState();
330
- // { temperature: 22.5, humidity: 45, battery: 85 }
420
+ const reading = await sensor.refreshState();
421
+ // { value: 22.5, numericValue: 22.5, unit: '°C', lastUpdated: Date }
422
+
423
+ // Cached accessors reflect the most recently fetched or externally updated state
424
+ console.log(sensor.value, sensor.numericValue, sensor.unit, sensor.lastUpdated);
331
425
  ```
332
426
 
333
427
  ### Protocol Direct Access
@@ -343,7 +437,7 @@ const caps = await escl.getCapabilities();
343
437
  const result = await escl.scan({ source: 'flatbed', resolution: 300 });
344
438
 
345
439
  // Direct IPP access
346
- const ipp = new IppProtocol('ipp://192.168.1.100:631/ipp/print');
440
+ const ipp = new IppProtocol('192.168.1.100', 631, '/ipp/print');
347
441
  const printerAttrs = await ipp.getPrinterAttributes();
348
442
 
349
443
  // Direct SNMP access
@@ -360,7 +454,7 @@ import { HomeAssistantProtocol, HomeAssistantDiscovery } from '@ecobridge.xyz/de
360
454
  const ha = new HomeAssistantProtocol({
361
455
  host: 'homeassistant.local',
362
456
  port: 8123,
363
- accessToken: 'your_long_lived_access_token',
457
+ token: 'your_long_lived_access_token',
364
458
  });
365
459
 
366
460
  await ha.connect();
@@ -369,22 +463,25 @@ await ha.connect();
369
463
  const entities = await ha.getStates();
370
464
 
371
465
  // Control a light
372
- await ha.callService('light', 'turn_on', {
373
- entity_id: 'light.living_room',
374
- brightness: 200,
375
- });
466
+ await ha.callService(
467
+ 'light',
468
+ 'turn_on',
469
+ { entity_id: 'light.living_room' },
470
+ { brightness: 200 }
471
+ );
376
472
 
377
473
  // Subscribe to state changes
378
- ha.on('state_changed', (event) => {
379
- console.log(`${event.entity_id}: ${event.new_state.state}`);
474
+ await ha.subscribeToStateChanges();
475
+ ha.on('state:changed', (event) => {
476
+ console.log(`${event.entity_id}: ${event.new_state?.state ?? 'removed'}`);
380
477
  });
381
478
 
382
479
  // Auto-discover Home Assistant instances via mDNS
383
480
  const discovery = new HomeAssistantDiscovery();
384
- discovery.on('found', (instance) => {
385
- console.log(`Found HA at ${instance.address}:${instance.port}`);
481
+ discovery.on('instance:found', (instance) => {
482
+ console.log(`Found HA at ${instance.host}:${instance.port}`);
386
483
  });
387
- await discovery.start();
484
+ await discovery.startMdnsDiscovery();
388
485
  ```
389
486
 
390
487
  ### Helper Utilities
@@ -405,7 +502,7 @@ const result = await withRetry(
405
502
 
406
503
  // IP utilities
407
504
  isValidIp('192.168.1.1'); // true
408
- cidrToIps('192.168.1.0/30'); // ['192.168.1.0', '192.168.1.1', ...]
505
+ cidrToIps('192.168.1.0/30'); // ['192.168.1.1', '192.168.1.2']
409
506
  getLocalSubnet(); // '192.168.1.0/24'
410
507
  ```
411
508
 
@@ -425,8 +522,8 @@ The library supports multiple discovery mechanisms:
425
522
  ```typescript
426
523
  import { SERVICE_TYPES } from '@ecobridge.xyz/devicemanager';
427
524
 
428
- // Available: escl, ipp, ipp-tls, airplay, raop,
429
- // googlecast, sonos, sane, http, https, printer
525
+ // Keys: ESCL, ESCL_SECURE, SANE, IPP, IPPS, PDL,
526
+ // AIRPLAY, RAOP, SONOS, GOOGLECAST, SPOTIFY
430
527
  ```
431
528
 
432
529
  ### SSDP Service Types
@@ -434,11 +531,12 @@ import { SERVICE_TYPES } from '@ecobridge.xyz/devicemanager';
434
531
  ```typescript
435
532
  import { SSDP_SERVICE_TYPES } from '@ecobridge.xyz/devicemanager';
436
533
 
437
- // Available: all, rootdevice, mediaRenderer, mediaServer,
438
- // contentDirectory, avtransport, renderingControl,
439
- // connectionManager, zonePlayer
534
+ // Root, DLNA media renderer/server, Sonos ZonePlayer,
535
+ // UPnP basic device and internet gateway service types
440
536
  ```
441
537
 
538
+ `SsdpDiscovery` uses native UDP sockets bound to every non-internal IPv4 interface. Searches are repeated every 30 seconds, and `search(serviceType)` can send an additional focused M-SEARCH while discovery is running. `start()` and `stop()` are idempotent and release all sockets, timers, and pending description requests.
539
+
442
540
  ## 🎯 Feature Types
443
541
 
444
542
  ```typescript
@@ -492,7 +590,7 @@ const printer = createPrinter({
492
590
  txtRecords: { rp: '/ipp/print' },
493
591
  });
494
592
 
495
- // Create a Sonos speaker
593
+ // Create a normalized Sonos speaker model (no live protocol client is created)
496
594
  const speaker = createSpeaker({
497
595
  id: 'living-room-sonos',
498
596
  name: 'Living Room',
@@ -504,40 +602,21 @@ const speaker = createSpeaker({
504
602
 
505
603
  ### Smart Home Factory Functions
506
604
 
507
- ```typescript
508
- import {
509
- createSmartLight,
510
- createSmartSwitch,
511
- createSmartSensor,
512
- createSmartClimate,
513
- createSmartCover,
514
- createSmartLock,
515
- createSmartFan,
516
- createSmartCamera,
517
- } from '@ecobridge.xyz/devicemanager';
518
-
519
- // Create devices with Home Assistant integration
520
- const light = createSmartLight({
521
- id: 'living-room-light',
522
- name: 'Living Room Light',
523
- address: 'homeassistant.local',
524
- port: 8123,
525
- entityId: 'light.living_room',
526
- protocol: 'home-assistant',
527
- protocolClient: haClient, // Your HomeAssistantProtocol instance
528
- capabilities: {
529
- supportsBrightness: true,
530
- supportsColorTemp: true,
531
- supportsRgb: true,
532
- },
533
- });
534
- ```
605
+ The smart-home factories require a `protocolClient` that implements the corresponding feature interface, such as `ILightProtocolClient` for `createSmartLight()`. `HomeAssistantProtocol` exposes Home Assistant-specific service methods and entity state shapes, so it cannot be passed directly as that client; use a protocol adapter that implements the required interface.
535
606
 
536
607
  ### Event Handling
537
608
 
609
+ `DeviceManager`, `UniversalDevice`, every `Feature`, and host-side providers
610
+ remain Node `EventEmitter` instances and additionally expose `events$`. Each
611
+ observable item contains the event `name`, its `args`, and `emittedAt`.
612
+
538
613
  ```typescript
539
614
  const manager = new DeviceManager();
540
615
 
616
+ const subscription = manager.events$.subscribe((event) => {
617
+ console.log(event.name, event.args, event.emittedAt);
618
+ });
619
+
541
620
  // Discovery events
542
621
  manager.on('device:found', ({ device, featureType }) => {
543
622
  console.log(`Found ${device.name} with ${featureType} capability`);
@@ -560,6 +639,8 @@ device.on('status:changed', ({ oldStatus, newStatus }) => {
560
639
  device.on('feature:connected', (featureType) => {
561
640
  console.log(`Feature ${featureType} connected`);
562
641
  });
642
+
643
+ subscription.unsubscribe();
563
644
  ```
564
645
 
565
646
  ### Error Handling
@@ -596,15 +677,13 @@ const maybePrint = device.getFeature<PrintFeature>('print'); // undefined
596
677
 
597
678
  Built with ❤️ using:
598
679
  - [bonjour-service](https://github.com/onlxltd/bonjour-service) - mDNS discovery
599
- - [node-ssdp](https://github.com/diversario/node-ssdp) - SSDP/UPnP discovery
680
+ - [Node.js networking and fetch APIs](https://nodejs.org/api/) - SSDP, IPP, eSCL, UPnP, and active network discovery
600
681
  - [net-snmp](https://github.com/markabrahams/node-net-snmp) - SNMP protocol
601
- - [ipp](https://github.com/niclaslindstedt/ipp) - IPP printing protocol
602
- - [sonos](https://github.com/bencevans/node-sonos) - Sonos control
603
- - [castv2-client](https://github.com/thibauts/node-castv2-client) - Chromecast protocol
682
+ - [ws](https://github.com/websockets/ws) - Home Assistant WebSocket transport
604
683
 
605
684
  ## License and Legal Information
606
685
 
607
- This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
686
+ This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license.md](./license.md) file.
608
687
 
609
688
  **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
610
689