@ecobridge.xyz/devicemanager 3.0.2 → 3.1.1

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 (42) hide show
  1. package/.gitea/workflows/default_tags.yaml +0 -21
  2. package/.smartconfig.json +69 -0
  3. package/changelog.md +23 -1
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/devicemanager.classes.devicemanager.js +33 -7
  6. package/dist_ts/discovery/discovery.classes.mdns.js +46 -11
  7. package/dist_ts/discovery/discovery.classes.networkscanner.d.ts +1 -1
  8. package/dist_ts/discovery/discovery.classes.networkscanner.js +1 -1
  9. package/dist_ts/discovery/discovery.classes.ssdp.d.ts +52 -21
  10. package/dist_ts/discovery/discovery.classes.ssdp.js +407 -117
  11. package/dist_ts/features/feature.print.d.ts +4 -2
  12. package/dist_ts/features/feature.print.js +48 -38
  13. package/dist_ts/index.d.ts +1 -1
  14. package/dist_ts/interfaces/feature.interfaces.d.ts +1 -1
  15. package/dist_ts/interfaces/index.d.ts +1 -1
  16. package/dist_ts/plugins.d.ts +5 -15
  17. package/dist_ts/plugins.js +6 -19
  18. package/dist_ts/protocols/index.d.ts +1 -1
  19. package/dist_ts/protocols/index.js +2 -2
  20. package/dist_ts/protocols/protocol.escl.js +2 -2
  21. package/dist_ts/protocols/protocol.ipp.d.ts +144 -33
  22. package/dist_ts/protocols/protocol.ipp.js +942 -239
  23. package/license.md +21 -0
  24. package/package.json +16 -25
  25. package/pnpm-workspace.yaml +4 -0
  26. package/readme.md +61 -76
  27. package/test/{test.ts → test.node.ts} +20 -45
  28. package/test/test.ssdp.node.ts +978 -0
  29. package/ts/00_commitinfo_data.ts +1 -1
  30. package/ts/devicemanager.classes.devicemanager.ts +44 -6
  31. package/ts/discovery/discovery.classes.mdns.ts +40 -11
  32. package/ts/discovery/discovery.classes.networkscanner.ts +1 -1
  33. package/ts/discovery/discovery.classes.ssdp.ts +493 -130
  34. package/ts/features/feature.print.ts +53 -45
  35. package/ts/index.ts +1 -0
  36. package/ts/interfaces/feature.interfaces.ts +1 -1
  37. package/ts/interfaces/index.ts +1 -1
  38. package/ts/plugins.ts +5 -25
  39. package/ts/protocols/index.ts +6 -1
  40. package/ts/protocols/protocol.escl.ts +1 -1
  41. package/ts/protocols/protocol.ipp.ts +1183 -260
  42. package/npmextra.json +0 -24
package/license.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Task Venture Capital GmbH
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecobridge.xyz/devicemanager",
3
- "version": "3.0.2",
3
+ "version": "3.1.1",
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",
@@ -8,32 +8,23 @@
8
8
  "type": "module",
9
9
  "author": "Task Venture Capital GmbH",
10
10
  "license": "MIT",
11
+ "devDependencies": {
12
+ "@git.zone/tsbuild": "4.4.2",
13
+ "@git.zone/tsrun": "2.0.6",
14
+ "@git.zone/tstest": "4.0.0",
15
+ "@types/node": "26.2.0",
16
+ "@types/ws": "8.18.1"
17
+ },
18
+ "dependencies": {
19
+ "@push.rocks/smartdelay": "3.1.0",
20
+ "@push.rocks/smartpath": "6.0.0",
21
+ "bonjour-service": "1.4.4",
22
+ "net-snmp": "3.26.3",
23
+ "ws": "8.21.1"
24
+ },
11
25
  "scripts": {
12
26
  "test": "(tstest test/ --verbose)",
13
27
  "build": "(tsbuild --web --allowimplicitany)",
14
28
  "buildDocs": "(tsdoc)"
15
- },
16
- "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"
22
- },
23
- "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"
38
29
  }
39
- }
30
+ }
@@ -0,0 +1,4 @@
1
+ allowBuilds:
2
+ esbuild: true
3
+ mongodb-memory-server: false
4
+ puppeteer: false
package/readme.md CHANGED
@@ -7,14 +7,14 @@ 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
13
  - 🖨️ **Scanners** — eSCL (AirScan), SANE protocols
14
- - 📄 **Printers** — IPP, JetDirect protocols
15
- - 🔊 **Speakers** — Sonos, AirPlay, Chromecast, DLNA
14
+ - 📄 **Printers** — IPP discovery and print execution; JetDirect/raw port detection
15
+ - 🔊 **Speakers** — Sonos, AirPlay, Chromecast, and DLNA discovery with normalized feature/factory modeling
16
16
  - 🔋 **UPS Systems** — NUT, SNMP protocols
17
- - 📡 **SNMP Devices** — Generic SNMP v1/v2c/v3 support
17
+ - 📡 **SNMP Devices** — Community-based SNMP v1 and v2c sessions; v3 is not implemented
18
18
  - 🏠 **Smart Home** — Home Assistant integration (lights, switches, sensors, climate, locks, fans, cameras, covers)
19
19
 
20
20
  ## Issue Reporting and Security
@@ -119,13 +119,14 @@ The central orchestrator for device discovery and management.
119
119
 
120
120
  ```typescript
121
121
  const manager = new DeviceManager({
122
- autoDiscovery: true, // Enable mDNS/SSDP auto-discovery
123
122
  discoveryTimeout: 10000, // Discovery timeout in ms
124
123
  enableRetry: true, // Enable retry with exponential backoff
125
124
  maxRetries: 5, // Maximum retry attempts
126
125
  });
127
126
  ```
128
127
 
128
+ 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.
129
+
129
130
  #### Discovery Methods
130
131
 
131
132
  ```typescript
@@ -240,6 +241,8 @@ await fs.writeFile('scan.jpg', result.data);
240
241
 
241
242
  #### 📄 PrintFeature
242
243
 
244
+ `PrintFeature.print()` submits jobs through IPP. Network discovery can identify an open JetDirect/raw port at TCP 9100, but JetDirect print execution is not implemented.
245
+
243
246
  ```typescript
244
247
  const printFeature = device.selectFeature<PrintFeature>('print');
245
248
  await printFeature.connect();
@@ -257,31 +260,33 @@ const job = await printFeature.print(pdfBuffer, {
257
260
  jobName: 'My Document',
258
261
  });
259
262
 
260
- // Check job status
261
- const status = await printFeature.getJobStatus(job.id);
263
+ // Get current job information
264
+ const jobInfo = await printFeature.getJobInfo(job.id);
262
265
  ```
263
266
 
264
267
  #### 🔊 PlaybackFeature & VolumeFeature
265
268
 
269
+ `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.
270
+
266
271
  ```typescript
267
272
  const playback = device.selectFeature<PlaybackFeature>('playback');
268
273
  const volume = device.selectFeature<VolumeFeature>('volume');
269
274
  await playback.connect();
270
275
 
271
- // Control playback
276
+ // Update normalized playback state
272
277
  await playback.play('http://example.com/audio.mp3');
273
278
  await playback.pause();
274
279
  await playback.stop();
275
280
  await playback.seek(120); // seconds
276
281
 
277
- // Get playback status
278
- const status = await playback.getStatus();
279
- // { state: 'playing', position: 45, duration: 180, track: { title: '...' } }
282
+ // Get normalized playback status
283
+ const status = await playback.getPlaybackStatus();
284
+ // { state: 'stopped', position: 120, duration: 0, track: undefined }
280
285
 
281
- // Control volume
286
+ // Update normalized volume state
282
287
  await volume.setVolume(50); // 0-100
283
- await volume.mute();
284
- await volume.unmute();
288
+ await volume.setMute(true);
289
+ await volume.toggleMute();
285
290
  const level = await volume.getVolume();
286
291
  ```
287
292
 
@@ -293,14 +298,16 @@ await power.connect();
293
298
 
294
299
  // Get UPS status
295
300
  const status = await power.getStatus();
296
- // { status: 'online', battery: { charge: 100, runtime: 1800, voltage: 13.8 }, ... }
301
+ // 'online' | 'onbattery' | 'lowbattery' | ... | 'unknown'
297
302
 
298
303
  // Get battery info
299
304
  const battery = await power.getBatteryInfo();
300
- // { charge: 95, runtime: 1500, health: 'good' }
305
+ // { charge: 0, runtime: 0 } until a protocol-specific implementation updates it
301
306
 
302
- // Run self-test
303
- await power.runTest();
307
+ // Run a battery test when supported
308
+ if (power.supportsTest) {
309
+ await power.testBattery();
310
+ }
304
311
  ```
305
312
 
306
313
  #### 🏠 Smart Home Features
@@ -309,9 +316,9 @@ await power.runTest();
309
316
  // Light control
310
317
  const light = device.selectFeature<LightFeature>('light');
311
318
  await light.turnOn();
312
- await light.setBrightness(80);
313
- await light.setColor({ r: 255, g: 100, b: 50 });
314
- await light.setColorTemperature(4000); // Kelvin
319
+ await light.setBrightness(80); // 0-255
320
+ await light.setRgbColor(255, 100, 50);
321
+ await light.setColorTemp(4000); // Kelvin
315
322
 
316
323
  // Switch control
317
324
  const switch_ = device.selectFeature<SwitchFeature>('switch');
@@ -321,13 +328,16 @@ await switch_.toggle();
321
328
 
322
329
  // Climate control
323
330
  const climate = device.selectFeature<ClimateFeature>('climate');
324
- await climate.setTargetTemperature(22);
325
- await climate.setMode('heat'); // 'heat' | 'cool' | 'auto' | 'off'
331
+ await climate.setTargetTemp(22);
332
+ await climate.setHvacMode('heat'); // 'heat' | 'cool' | 'auto' | 'off'
326
333
 
327
334
  // Sensor reading
328
335
  const sensor = device.selectFeature<SensorFeature>('sensor');
329
- const reading = await sensor.getState();
330
- // { temperature: 22.5, humidity: 45, battery: 85 }
336
+ const reading = await sensor.refreshState();
337
+ // { value: 22.5, numericValue: 22.5, unit: '°C', lastUpdated: Date }
338
+
339
+ // Cached accessors reflect the most recently fetched or externally updated state
340
+ console.log(sensor.value, sensor.numericValue, sensor.unit, sensor.lastUpdated);
331
341
  ```
332
342
 
333
343
  ### Protocol Direct Access
@@ -343,7 +353,7 @@ const caps = await escl.getCapabilities();
343
353
  const result = await escl.scan({ source: 'flatbed', resolution: 300 });
344
354
 
345
355
  // Direct IPP access
346
- const ipp = new IppProtocol('ipp://192.168.1.100:631/ipp/print');
356
+ const ipp = new IppProtocol('192.168.1.100', 631, '/ipp/print');
347
357
  const printerAttrs = await ipp.getPrinterAttributes();
348
358
 
349
359
  // Direct SNMP access
@@ -360,7 +370,7 @@ import { HomeAssistantProtocol, HomeAssistantDiscovery } from '@ecobridge.xyz/de
360
370
  const ha = new HomeAssistantProtocol({
361
371
  host: 'homeassistant.local',
362
372
  port: 8123,
363
- accessToken: 'your_long_lived_access_token',
373
+ token: 'your_long_lived_access_token',
364
374
  });
365
375
 
366
376
  await ha.connect();
@@ -369,22 +379,25 @@ await ha.connect();
369
379
  const entities = await ha.getStates();
370
380
 
371
381
  // Control a light
372
- await ha.callService('light', 'turn_on', {
373
- entity_id: 'light.living_room',
374
- brightness: 200,
375
- });
382
+ await ha.callService(
383
+ 'light',
384
+ 'turn_on',
385
+ { entity_id: 'light.living_room' },
386
+ { brightness: 200 }
387
+ );
376
388
 
377
389
  // Subscribe to state changes
378
- ha.on('state_changed', (event) => {
379
- console.log(`${event.entity_id}: ${event.new_state.state}`);
390
+ await ha.subscribeToStateChanges();
391
+ ha.on('state:changed', (event) => {
392
+ console.log(`${event.entity_id}: ${event.new_state?.state ?? 'removed'}`);
380
393
  });
381
394
 
382
395
  // Auto-discover Home Assistant instances via mDNS
383
396
  const discovery = new HomeAssistantDiscovery();
384
- discovery.on('found', (instance) => {
385
- console.log(`Found HA at ${instance.address}:${instance.port}`);
397
+ discovery.on('instance:found', (instance) => {
398
+ console.log(`Found HA at ${instance.host}:${instance.port}`);
386
399
  });
387
- await discovery.start();
400
+ await discovery.startMdnsDiscovery();
388
401
  ```
389
402
 
390
403
  ### Helper Utilities
@@ -405,7 +418,7 @@ const result = await withRetry(
405
418
 
406
419
  // IP utilities
407
420
  isValidIp('192.168.1.1'); // true
408
- cidrToIps('192.168.1.0/30'); // ['192.168.1.0', '192.168.1.1', ...]
421
+ cidrToIps('192.168.1.0/30'); // ['192.168.1.1', '192.168.1.2']
409
422
  getLocalSubnet(); // '192.168.1.0/24'
410
423
  ```
411
424
 
@@ -425,8 +438,8 @@ The library supports multiple discovery mechanisms:
425
438
  ```typescript
426
439
  import { SERVICE_TYPES } from '@ecobridge.xyz/devicemanager';
427
440
 
428
- // Available: escl, ipp, ipp-tls, airplay, raop,
429
- // googlecast, sonos, sane, http, https, printer
441
+ // Keys: ESCL, ESCL_SECURE, SANE, IPP, IPPS, PDL,
442
+ // AIRPLAY, RAOP, SONOS, GOOGLECAST, SPOTIFY
430
443
  ```
431
444
 
432
445
  ### SSDP Service Types
@@ -434,11 +447,12 @@ import { SERVICE_TYPES } from '@ecobridge.xyz/devicemanager';
434
447
  ```typescript
435
448
  import { SSDP_SERVICE_TYPES } from '@ecobridge.xyz/devicemanager';
436
449
 
437
- // Available: all, rootdevice, mediaRenderer, mediaServer,
438
- // contentDirectory, avtransport, renderingControl,
439
- // connectionManager, zonePlayer
450
+ // Root, DLNA media renderer/server, Sonos ZonePlayer,
451
+ // UPnP basic device and internet gateway service types
440
452
  ```
441
453
 
454
+ `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.
455
+
442
456
  ## 🎯 Feature Types
443
457
 
444
458
  ```typescript
@@ -492,7 +506,7 @@ const printer = createPrinter({
492
506
  txtRecords: { rp: '/ipp/print' },
493
507
  });
494
508
 
495
- // Create a Sonos speaker
509
+ // Create a normalized Sonos speaker model (no live protocol client is created)
496
510
  const speaker = createSpeaker({
497
511
  id: 'living-room-sonos',
498
512
  name: 'Living Room',
@@ -504,34 +518,7 @@ const speaker = createSpeaker({
504
518
 
505
519
  ### Smart Home Factory Functions
506
520
 
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
- ```
521
+ 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
522
 
536
523
  ### Event Handling
537
524
 
@@ -596,15 +583,13 @@ const maybePrint = device.getFeature<PrintFeature>('print'); // undefined
596
583
 
597
584
  Built with ❤️ using:
598
585
  - [bonjour-service](https://github.com/onlxltd/bonjour-service) - mDNS discovery
599
- - [node-ssdp](https://github.com/diversario/node-ssdp) - SSDP/UPnP discovery
586
+ - [Node.js networking and fetch APIs](https://nodejs.org/api/) - SSDP, IPP, eSCL, UPnP, and active network discovery
600
587
  - [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
588
+ - [ws](https://github.com/websockets/ws) - Home Assistant WebSocket transport
604
589
 
605
590
  ## License and Legal Information
606
591
 
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.
592
+ 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
593
 
609
594
  **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
595
 
@@ -127,50 +127,6 @@ tap.test('withRetry should throw after max retries', async () => {
127
127
  expect(callCount).toEqual(3); // Initial + 2 retries
128
128
  });
129
129
 
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
130
  // Test Scanner creation using factory
175
131
  tap.test('should create Scanner device using factory', async () => {
176
132
  const scanner = devicemanager.createScanner({
@@ -204,8 +160,8 @@ tap.test('should create Printer device using factory', async () => {
204
160
  name: 'Test Printer',
205
161
  address: '192.168.1.101',
206
162
  port: 631,
207
- ippPath: '/ipp/print',
208
163
  txtRecords: {
164
+ 'rp': '/ipp/print',
209
165
  'ty': 'Brother HL-L2350DW',
210
166
  'Color': 'T',
211
167
  'Duplex': 'T',
@@ -221,6 +177,25 @@ tap.test('should create Printer device using factory', async () => {
221
177
  expect(printFeature).toBeInstanceOf(devicemanager.PrintFeature);
222
178
  });
223
179
 
180
+ tap.test('should create a Sonos speaker without an external Sonos client', async () => {
181
+ const speaker = devicemanager.createSpeaker({
182
+ id: 'test:speaker:sonos',
183
+ name: 'Living Room',
184
+ address: '192.168.1.102',
185
+ port: 1400,
186
+ protocol: 'sonos',
187
+ });
188
+
189
+ const playback = speaker.selectFeature<devicemanager.PlaybackFeature>('playback');
190
+ const volume = speaker.selectFeature<devicemanager.VolumeFeature>('volume');
191
+
192
+ expect(speaker.id).toEqual('test:speaker:sonos');
193
+ expect(playback.protocol).toEqual('sonos');
194
+ expect(playback.supportsQueue).toEqual(true);
195
+ expect(playback.supportsSeek).toEqual(true);
196
+ expect(volume.protocol).toEqual('sonos');
197
+ });
198
+
224
199
  // Test UniversalDevice feature management
225
200
  tap.test('should manage features on UniversalDevice', async () => {
226
201
  const device = new devicemanager.UniversalDevice('192.168.1.50', 80, {