@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.
- package/.gitea/workflows/default_tags.yaml +0 -21
- package/.smartconfig.json +69 -0
- package/changelog.md +38 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/device/device.classes.device.d.ts +2 -2
- package/dist_ts/device/device.classes.device.js +3 -3
- package/dist_ts/devicemanager.classes.devicemanager.d.ts +17 -4
- package/dist_ts/devicemanager.classes.devicemanager.js +99 -11
- package/dist_ts/discovery/discovery.classes.mdns.js +46 -11
- package/dist_ts/discovery/discovery.classes.networkscanner.d.ts +5 -1
- package/dist_ts/discovery/discovery.classes.networkscanner.js +44 -3
- package/dist_ts/discovery/discovery.classes.ssdp.d.ts +52 -21
- package/dist_ts/discovery/discovery.classes.ssdp.js +407 -117
- package/dist_ts/events/events.observable.d.ts +15 -0
- package/dist_ts/events/events.observable.js +22 -0
- package/dist_ts/events/index.d.ts +1 -0
- package/dist_ts/events/index.js +2 -0
- package/dist_ts/factories/index.js +7 -5
- package/dist_ts/features/feature.abstract.d.ts +2 -2
- package/dist_ts/features/feature.abstract.js +3 -3
- package/dist_ts/features/feature.print.d.ts +3 -0
- package/dist_ts/features/feature.print.js +17 -2
- package/dist_ts/features/feature.scan.d.ts +5 -2
- package/dist_ts/features/feature.scan.js +98 -27
- package/dist_ts/index.d.ts +4 -2
- package/dist_ts/index.js +4 -2
- package/dist_ts/interfaces/feature.interfaces.d.ts +22 -2
- package/dist_ts/interfaces/index.d.ts +22 -4
- package/dist_ts/interfaces/index.js +1 -1
- package/dist_ts/plugins.d.ts +6 -15
- package/dist_ts/plugins.js +7 -19
- package/dist_ts/protocols/index.d.ts +2 -1
- package/dist_ts/protocols/index.js +4 -2
- package/dist_ts/protocols/protocol.brother.d.ts +67 -0
- package/dist_ts/protocols/protocol.brother.js +560 -0
- package/dist_ts/protocols/protocol.escl.d.ts +13 -4
- package/dist_ts/protocols/protocol.escl.js +345 -178
- package/dist_ts/protocols/protocol.ipp.d.ts +2 -0
- package/dist_ts/protocols/protocol.ipp.js +22 -2
- package/dist_ts/providers/index.d.ts +1 -0
- package/dist_ts/providers/index.js +2 -0
- package/dist_ts/providers/provider.smb.d.ts +60 -0
- package/dist_ts/providers/provider.smb.js +224 -0
- package/license.md +21 -0
- package/package.json +15 -20
- package/pnpm-workspace.yaml +6 -0
- package/readme.md +157 -78
- package/test/test.brother.node.ts +306 -0
- package/test/test.escl.node.ts +233 -0
- package/test/test.ipp.node.ts +75 -0
- package/test/{test.ts → test.node.ts} +37 -45
- package/test/test.smb.node.ts +123 -0
- package/test/test.ssdp.node.ts +978 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/device/device.classes.device.ts +2 -2
- package/ts/devicemanager.classes.devicemanager.ts +125 -12
- package/ts/discovery/discovery.classes.mdns.ts +40 -11
- package/ts/discovery/discovery.classes.networkscanner.ts +51 -2
- package/ts/discovery/discovery.classes.ssdp.ts +493 -130
- package/ts/events/events.observable.ts +31 -0
- package/ts/events/index.ts +4 -0
- package/ts/factories/index.ts +7 -5
- package/ts/features/feature.abstract.ts +2 -2
- package/ts/features/feature.print.ts +16 -1
- package/ts/features/feature.scan.ts +93 -27
- package/ts/index.ts +21 -0
- package/ts/interfaces/feature.interfaces.ts +23 -2
- package/ts/interfaces/index.ts +23 -4
- package/ts/plugins.ts +6 -25
- package/ts/protocols/index.ts +11 -1
- package/ts/protocols/protocol.brother.ts +648 -0
- package/ts/protocols/protocol.escl.ts +348 -191
- package/ts/protocols/protocol.ipp.ts +24 -1
- package/ts/providers/index.ts +7 -0
- package/ts/providers/provider.smb.ts +326 -0
- package/dist_ts/protocols/protocol.ipp.old.d.ts +0 -45
- package/dist_ts/protocols/protocol.ipp.old.js +0 -284
- package/npmextra.json +0 -24
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecobridge.xyz/devicemanager",
|
|
3
|
-
"version": "3.
|
|
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": "
|
|
18
|
-
"@git.zone/tsrun": "
|
|
19
|
-
"@git.zone/tstest": "
|
|
20
|
-
"@types/node": "
|
|
21
|
-
"@types/ws": "
|
|
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": "
|
|
25
|
-
"@push.rocks/
|
|
26
|
-
"@push.rocks/
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
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
|
}
|
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
|
|
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
|
|
14
|
-
- 📄 **Printers** — IPP, JetDirect
|
|
15
|
-
-
|
|
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** —
|
|
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
|
-
│
|
|
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
|
-
//
|
|
261
|
-
const
|
|
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
|
-
//
|
|
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.
|
|
279
|
-
// { state: '
|
|
366
|
+
// Get normalized playback status
|
|
367
|
+
const status = await playback.getPlaybackStatus();
|
|
368
|
+
// { state: 'stopped', position: 120, duration: 0, track: undefined }
|
|
280
369
|
|
|
281
|
-
//
|
|
370
|
+
// Update normalized volume state
|
|
282
371
|
await volume.setVolume(50); // 0-100
|
|
283
|
-
await volume.
|
|
284
|
-
await volume.
|
|
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
|
-
//
|
|
385
|
+
// 'online' | 'onbattery' | 'lowbattery' | ... | 'unknown'
|
|
297
386
|
|
|
298
387
|
// Get battery info
|
|
299
388
|
const battery = await power.getBatteryInfo();
|
|
300
|
-
// { charge:
|
|
389
|
+
// { charge: 0, runtime: 0 } until a protocol-specific implementation updates it
|
|
301
390
|
|
|
302
|
-
// Run
|
|
303
|
-
|
|
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.
|
|
314
|
-
await light.
|
|
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.
|
|
325
|
-
await climate.
|
|
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.
|
|
330
|
-
// {
|
|
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('
|
|
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
|
-
|
|
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(
|
|
373
|
-
|
|
374
|
-
|
|
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.
|
|
379
|
-
|
|
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.
|
|
481
|
+
discovery.on('instance:found', (instance) => {
|
|
482
|
+
console.log(`Found HA at ${instance.host}:${instance.port}`);
|
|
386
483
|
});
|
|
387
|
-
await discovery.
|
|
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.
|
|
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
|
-
//
|
|
429
|
-
//
|
|
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
|
-
//
|
|
438
|
-
//
|
|
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
|
-
|
|
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
|
-
- [
|
|
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
|
-
- [
|
|
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 [
|
|
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
|
|