{"_id":"@ecobridge.xyz/devicemanager","_rev":"5-25101e2baadb5a8a787f09289b3c3315","name":"@ecobridge.xyz/devicemanager","dist-tags":{"latest":"4.0.1"},"versions":{"3.0.2":{"name":"@ecobridge.xyz/devicemanager","version":"3.0.2","author":{"name":"Task Venture Capital GmbH"},"license":"MIT","_id":"@ecobridge.xyz/devicemanager@3.0.2","maintainers":[{"name":"lossless","email":"hello@lossless.com"}],"dist":{"shasum":"b90d687cd5a0748a65d42de4ef5abfa4159359f1","tarball":"https://registry.npmjs.org/@ecobridge.xyz/devicemanager/-/devicemanager-3.0.2.tgz","fileCount":137,"integrity":"sha512-u0hhEOH9bwHwDICCu47I/k6XequeWP3wZvaB9jZYAyqq9Kgm+IK42vSrbplp+6rsl4Bg/vWPOpOL4P16282RIw==","signatures":[{"sig":"MEQCICQVQLOie8FfKTukrgui7MrZNZA4Qq1KrXyMPpfaI1LuAiBx8liikVc4DLglnwbhAc7X6xvRKdTnqZ+q7/0JFmU3TQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1306488},"main":"dist_ts/index.js","type":"module","gitHead":"716347bac1a45f866637e8ca9ebe1466cf51a343","private":false,"scripts":{"test":"(tstest test/ --verbose)","build":"(tsbuild --web --allowimplicitany)","buildDocs":"(tsdoc)"},"typings":"dist_ts/index.d.ts","_npmUser":{"name":"lossless","email":"hello@lossless.com"},"_npmVersion":"11.7.0","description":"a device manager for talking to devices on network and over usb","directories":{},"_nodeVersion":"25.2.1","dependencies":{"ws":"^8.19.0","ipp":"^2.0.1","sonos":"^1.14.2","net-snmp":"^3.26.0","node-ssdp":"^4.0.1","castv2-client":"^1.2.0","bonjour-service":"^1.3.0","@push.rocks/smartxml":"^2.0.0","@push.rocks/smartpath":"^6.0.0","@push.rocks/smartdelay":"^3.0.5","@push.rocks/smartevent":"^2.0.5","@push.rocks/smartnetwork":"^4.4.0","@push.rocks/smartpromise":"^4.2.3","@push.rocks/smartrequest":"^5.0.1"},"_hasShrinkwrap":false,"devDependencies":{"@types/ws":"^8.18.1","@types/node":"^25.0.3","@git.zone/tsrun":"^2.0.0","@git.zone/tstest":"^3.1.3","@git.zone/tsbuild":"^4.1.0"},"_npmOperationalInternal":{"tmp":"tmp/devicemanager_3.0.2_1768215629481_0.19546790884067455","host":"s3://npm-registry-packages-npm-production"}},"3.1.0":{"name":"@ecobridge.xyz/devicemanager","version":"3.1.0","author":{"name":"Task Venture Capital GmbH"},"license":"MIT","_id":"@ecobridge.xyz/devicemanager@3.1.0","maintainers":[{"name":"lossless","email":"hello@lossless.com"}],"dist":{"shasum":"81d5f098029235eaf7f83ab4e048fc39fb316403","tarball":"https://registry.npmjs.org/@ecobridge.xyz/devicemanager/-/devicemanager-3.1.0.tgz","fileCount":139,"integrity":"sha512-cE+fCEv4CeecfJSMbp02wtCRb/wkUCozEwd359Av3mYxbPDSV6VQlD7B88UbXEv92pU5J+clLt2IeW3YtYnuWw==","signatures":[{"sig":"MEQCIHdqvqXYLuYTTHOhys0Om9Agpu5fmdrHRwGlNRX8JtNiAiA8lmvgllgK1HwKZRmuUsSR++eJ78aW5K8gpmVsI/VgcQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1424800},"main":"dist_ts/index.js","type":"module","gitHead":"d9029ec02ba01e1cd3caeae4bae4bb7ea450f53c","private":false,"scripts":{"test":"(tstest test/ --verbose)","build":"(tsbuild --web --allowimplicitany)","buildDocs":"(tsdoc)"},"typings":"dist_ts/index.d.ts","_npmUser":{"name":"lossless","email":"hello@lossless.com"},"_npmVersion":"11.7.0","description":"a device manager for talking to devices on network and over usb","directories":{},"_nodeVersion":"25.2.1","dependencies":{"ws":"^8.19.0","ipp":"^2.0.1","sonos":"^1.14.2","net-snmp":"^3.26.0","node-ssdp":"^4.0.1","castv2-client":"^1.2.0","bonjour-service":"^1.3.0","@push.rocks/smartxml":"^2.0.0","@push.rocks/smartpath":"^6.0.0","@push.rocks/smartdelay":"^3.0.5","@push.rocks/smartevent":"^2.0.5","@push.rocks/smartnetwork":"^4.4.0","@push.rocks/smartpromise":"^4.2.3","@push.rocks/smartrequest":"^5.0.1"},"_hasShrinkwrap":false,"devDependencies":{"@types/ws":"^8.18.1","@types/node":"^25.0.3","@git.zone/tsrun":"^2.0.0","@git.zone/tstest":"^3.1.3","@git.zone/tsbuild":"^4.1.0"},"_npmOperationalInternal":{"tmp":"tmp/devicemanager_3.1.0_1768338878584_0.07660578214161773","host":"s3://npm-registry-packages-npm-production"}},"3.1.1":{"name":"@ecobridge.xyz/devicemanager","version":"3.1.1","author":"Task Venture Capital GmbH","license":"MIT","_id":"@ecobridge.xyz/devicemanager@3.1.1","maintainers":[{"name":"lossless","email":"hello@lossless.com"}],"dist":{"shasum":"297d673c8df514b4bafe3fa57ab090b5931fd1f8","tarball":"https://registry.npmjs.org/@ecobridge.xyz/devicemanager/-/devicemanager-3.1.1.tgz","fileCount":140,"integrity":"sha512-ohcMTBp55kokyPgsSNR57wTl9f46re0RCdIlmRqBNm83z996vUAkaDo6wJr1SLJJu8OmkFEa77C3WYO6bG/5WA==","signatures":[{"sig":"MEUCIQDYPXUNJtasknPP/lVGJnclOAhiNQMbt9kr7Xkc5Uz+SAIgf6iKo3lAUmzIMZp39GJe/n3xkhgwdXOTrrua1tPtJTU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1485954},"main":"dist_ts/index.js","type":"module","private":false,"scripts":{"test":"(tstest test/ --verbose)","build":"(tsbuild --web --allowimplicitany)","buildDocs":"(tsdoc)"},"typings":"dist_ts/index.d.ts","_npmUser":{"name":"lossless","email":"hello@lossless.com"},"description":"a device manager for talking to devices on network and over usb","directories":{},"_nodeVersion":"25.2.1","dependencies":{"ws":"8.21.1","net-snmp":"3.26.3","bonjour-service":"1.4.4","@push.rocks/smartpath":"6.0.0","@push.rocks/smartdelay":"3.1.0"},"_hasShrinkwrap":false,"devDependencies":{"@types/ws":"8.18.1","@types/node":"26.2.0","@git.zone/tsrun":"2.0.6","@git.zone/tstest":"4.0.0","@git.zone/tsbuild":"4.4.2"},"_npmOperationalInternal":{"tmp":"tmp/devicemanager_3.1.1_1786308360403_0.4385445404616193","host":"s3://npm-registry-packages-npm-production"}},"3.2.0":{"name":"@ecobridge.xyz/devicemanager","version":"3.2.0","author":{"name":"Task Venture Capital GmbH"},"license":"MIT","_id":"@ecobridge.xyz/devicemanager@3.2.0","maintainers":[{"name":"lossless","email":"hello@lossless.com"}],"dist":{"shasum":"c6d8c4ba1f01699e6e7e86a74d82162ccac90653","tarball":"https://registry.npmjs.org/@ecobridge.xyz/devicemanager/-/devicemanager-3.2.0.tgz","fileCount":159,"integrity":"sha512-oaF+fNm5Wl6+I+0xEpSRNjDqUA8RkJGOIv3lyPciwUaDmeR/FfRNZndNjHetHRqKCIhvanIcvPvVaFfXtqFZgw==","signatures":[{"sig":"MEUCIQD0BAcjhighnR7/oA5yT65rTcmGU/Ra4raIZQlOaLQRjAIgIpyPp5qFK5e7DZX3Tngx+1EXF6mS0aDJpQdCFmb6vaQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1700270},"main":"dist_ts/index.js","type":"module","_from":"file:ecobridge.xyz-devicemanager-3.2.0.tgz","private":false,"scripts":{"test":"(tstest test/ --verbose)","build":"(tsbuild --web --allowimplicitany)","buildDocs":"(tsdoc)"},"typings":"dist_ts/index.d.ts","_npmUser":{"name":"lossless","email":"hello@lossless.com"},"_resolved":"/Users/philkunz/foss.global/ecobridge.xyz/devicemanager/ecobridge.xyz-devicemanager-3.2.0.tgz","_integrity":"sha512-oaF+fNm5Wl6+I+0xEpSRNjDqUA8RkJGOIv3lyPciwUaDmeR/FfRNZndNjHetHRqKCIhvanIcvPvVaFfXtqFZgw==","_npmVersion":"11.6.1","description":"a device manager for talking to devices on network and over usb","directories":{},"_nodeVersion":"24.7.0","dependencies":{"ws":"8.21.1","rxjs":"7.8.2","net-snmp":"3.26.3","bonjour-service":"1.4.4","fast-xml-parser":"4.5.7","@push.rocks/smartpath":"6.0.0","@push.rocks/smartdelay":"3.1.0","@push.rocks/smartsamba":"0.3.1"},"_hasShrinkwrap":false,"packageManager":"pnpm@11.21.0","devDependencies":{"@types/ws":"8.18.1","@types/node":"26.2.0","@git.zone/tsrun":"2.0.6","@git.zone/tstest":"4.0.0","@git.zone/tsbuild":"4.4.2"},"_npmOperationalInternal":{"tmp":"tmp/devicemanager_3.2.0_1788529453253_0.5064460554774073","host":"s3://npm-registry-packages-npm-production"}},"4.0.1":{"name":"@ecobridge.xyz/devicemanager","version":"4.0.1","private":false,"description":"a device manager for talking to devices on network and over usb","main":"dist_ts/index.js","typings":"dist_ts/index.d.ts","type":"module","author":"Task Venture Capital GmbH","license":"MIT","devDependencies":{"@git.zone/tsbuild":"4.4.2","@git.zone/tsrun":"2.0.6","@git.zone/tstest":"4.0.0","@types/node":"26.2.0","@types/ws":"8.18.1"},"dependencies":{"@push.rocks/smartdelay":"3.1.0","@push.rocks/smartpath":"6.0.0","@push.rocks/smartsamba":"0.3.1","bonjour-service":"1.4.4","fast-xml-parser":"4.5.7","net-snmp":"3.26.3","rxjs":"7.8.2","ws":"8.21.1"},"scripts":{"test":"(tstest test/ --verbose)","build":"(tsbuild --web --allowimplicitany)","buildDocs":"(tsdoc)"},"_nodeVersion":"25.2.1","_id":"@ecobridge.xyz/devicemanager@4.0.1","dist":{"integrity":"sha512-aptkN+9i7q0TCGHMExsX1mZ/Lkjyv6Ps6uaGi+/veEeYjyR3xTPjQi+7xElnDsteKrQnnFOZsUTFmYDzlPd/TQ==","shasum":"def8bb84f1b5a67aad696e91bcc566d2f1a67f50","tarball":"https://registry.npmjs.org/@ecobridge.xyz/devicemanager/-/devicemanager-4.0.1.tgz","fileCount":173,"unpackedSize":1850238,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIHVKetoV46f3elt3QY8qkTSo6g2wV+AICNEW005JcsKpAiEAkKRiMNS5uzVr9KITQmgiaOxocBbn2l4cRSA6GY37Vjs="}]},"_npmUser":{"name":"lossless","email":"hello@lossless.com"},"directories":{},"maintainers":[{"name":"lossless","email":"hello@lossless.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/devicemanager_4.0.1_1788870074569_0.3939093714377666"},"_hasShrinkwrap":false}},"time":{"created":"2026-01-12T11:00:29.352Z","modified":"2026-09-08T12:21:14.923Z","3.0.2":"2026-01-12T11:00:29.676Z","3.1.0":"2026-01-13T21:14:38.786Z","3.1.1":"2026-08-09T20:46:00.560Z","3.2.0":"2026-09-04T13:44:13.467Z","4.0.1":"2026-09-08T12:21:14.704Z"},"author":"Task Venture Capital GmbH","license":"MIT","description":"a device manager for talking to devices on network and over usb","maintainers":[{"name":"lossless","email":"hello@lossless.com"}],"readme":"# @ecobridge.xyz/devicemanager\n\nA comprehensive, TypeScript-first device manager for discovering and communicating with network devices. 🔌\n\n[![npm version](https://img.shields.io/npm/v/@ecobridge.xyz/devicemanager.svg)](https://www.npmjs.com/package/@ecobridge.xyz/devicemanager)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n## 🎯 Overview\n\n`@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.\n\n**Supported Device Types:**\n- 🖨️ **Scanners** — Brother native network scanning, eSCL (AirScan), and SANE\n- 📄 **Printers** — IPP/AirPrint discovery, capabilities, jobs, cancellation, and print execution; JetDirect/raw port detection\n- 📁 **SMB Shares** — Host local folders through smartsamba and emit settled document arrivals through RxJS\n- 🔊 **Speakers** — Sonos, AirPlay, Chromecast, and DLNA discovery; live transport and volume control over UPnP for Sonos and DLNA, plus announcements that interrupt and restore playback\n- 🔋 **UPS Systems** — NUT, SNMP protocols\n- 📡 **SNMP Devices** — Community-based SNMP v1 and v2c sessions; v3 is not implemented\n- 🏠 **Smart Home** — Home Assistant integration (lights, switches, sensors, climate, locks, fans, cameras, covers)\n\n## Issue Reporting and Security\n\nFor reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.\n\n## 📦 Installation\n\n```bash\n# Using pnpm (recommended)\npnpm add @ecobridge.xyz/devicemanager\n\n# Using npm\nnpm install @ecobridge.xyz/devicemanager\n\n# Using yarn\nyarn add @ecobridge.xyz/devicemanager\n```\n\n## 🚀 Quick Start\n\n### The OOP Pattern: Discovery → Selection → Feature → Operation\n\n```typescript\nimport { DeviceManager, ScanFeature } from '@ecobridge.xyz/devicemanager';\nimport * as fs from 'fs/promises';\n\nasync function scanDocument() {\n  const manager = new DeviceManager();\n\n  // 1️⃣ DISCOVERY - Find scanners in your network\n  await manager.discoverScanners('192.168.1.0/24');\n\n  // 2️⃣ INSPECTION - See what's available\n  const scanners = manager.getDevices({ hasFeature: 'scan' });\n  console.log('Found scanners:', scanners.map(s => `${s.name} at ${s.address}`));\n\n  // 3️⃣ SELECTION - Choose your device (explicit, no magic!)\n  const device = manager.selectDevice({ address: '192.168.1.100' });\n\n  // 4️⃣ FEATURE ACCESS - Get the capability you need\n  const scanFeature = device.selectFeature<ScanFeature>('scan');\n\n  // 5️⃣ OPERATION - Do the thing!\n  await scanFeature.connect();\n  const result = await scanFeature.scan({\n    source: 'flatbed',\n    resolution: 300,\n    colorMode: 'color',\n    format: 'jpeg',\n  });\n\n  await fs.writeFile('scan.jpg', result.data);\n  console.log(`Saved: scan.jpg (${result.data.length} bytes)`);\n\n  await manager.shutdown();\n}\n\nscanDocument();\n```\n\n## 🏗️ Architecture\n\nThe library follows a clean, composable architecture:\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│                      DeviceManager                          │\n│  • Discovery (mDNS, SSDP, Network Scanning)                │\n│  • Device Registry (by IP, deduplication)                  │\n│  • Device Selection (query & assert patterns)              │\n└─────────────────────────────────────────────────────────────┘\n                              │\n                              ▼\n┌─────────────────────────────────────────────────────────────┐\n│                     UniversalDevice                         │\n│  • Represents any network device                           │\n│  • Composable features (scan, print, volume, etc.)         │\n│  • Connection lifecycle management                         │\n└─────────────────────────────────────────────────────────────┘\n                              │\n                              ▼\n┌─────────────────────────────────────────────────────────────┐\n│                        Features                             │\n│  ScanFeature │ PrintFeature │ PlaybackFeature │ VolumeFeature\n│  PowerFeature │ SnmpFeature │ LightFeature │ SwitchFeature  │\n│  SensorFeature │ ClimateFeature │ CameraFeature │ ...       │\n└─────────────────────────────────────────────────────────────┘\n                              │\n                              ▼\n┌─────────────────────────────────────────────────────────────┐\n│                        Protocols                            │\n│ Brother Scan │ eSCL │ SANE │ IPP │ SNMP │ NUT │ UPnP/SOAP  │\n└─────────────────────────────────────────────────────────────┘\n```\n\n## 📖 API Reference\n\n### DeviceManager\n\nThe central orchestrator for device discovery and management.\n\n```typescript\nconst manager = new DeviceManager({\n  discoveryTimeout: 10000,  // Discovery timeout in ms\n  enableRetry: true,        // Enable retry with exponential backoff\n  maxRetries: 5,            // Maximum retry attempts\n});\n```\n\nConstructing `DeviceManager` does not initiate network discovery. Call `startDiscovery()` explicitly when continuous mDNS/SSDP discovery should begin; the `autoDiscovery` option does not currently trigger startup.\n\n#### Discovery Methods\n\n```typescript\n// Focused scanner discovery\nconst scanners = await manager.discoverScanners('192.168.1.0/24', {\n  timeout: 3000,\n  concurrency: 50,\n});\n\n// Focused printer discovery\nconst printers = await manager.discoverPrinters('192.168.1.0/24');\n\n// General network scan (all device types)\nconst devices = await manager.scanNetwork({\n  ipRange: '192.168.1.0/24',\n  probeBrother: true,\n  probeEscl: true,\n  probeIpp: true,\n  probeSane: true,\n  probeAirplay: true,\n  probeSonos: true,\n  probeChromecast: true,\n});\n\n// mDNS/SSDP continuous discovery\nawait manager.startDiscovery();\nmanager.on('device:found', ({ device, featureType }) => {\n  console.log(`Found: ${device.name}`);\n});\nawait manager.stopDiscovery();\n```\n\n#### Device Selection\n\n```typescript\n// Query pattern (returns array, may be empty)\nconst allDevices = manager.getDevices();\nconst scanners = manager.getDevices({ hasFeature: 'scan' });\nconst brotherDevices = manager.getDevices({ name: 'Brother' });\n\n// Assert pattern (returns single device, throws if not found)\nconst device = manager.selectDevice({ address: '192.168.1.100' });\nconst scanner = manager.selectDevice({ name: 'Brother', hasFeature: 'scan' });\n```\n\n#### IDeviceSelector Interface\n\n```typescript\ninterface IDeviceSelector {\n  id?: string;               // Exact match on device ID\n  address?: string;          // Exact match on IP address\n  name?: string;             // Partial match (case-insensitive)\n  model?: string;            // Partial match (case-insensitive)\n  manufacturer?: string;     // Partial match (case-insensitive)\n  hasFeature?: TFeatureType; // Must have this feature\n  hasFeatures?: TFeatureType[];   // Must have ALL features\n  hasAnyFeature?: TFeatureType[]; // Must have ANY feature\n}\n```\n\n### UniversalDevice\n\nRepresents any network device with composable features.\n\n```typescript\nconst device = manager.selectDevice({ address: '192.168.1.100' });\n\n// Device properties\nconsole.log(device.name);         // \"Brother MFC-J5730DW\"\nconsole.log(device.address);      // \"192.168.1.100\"\nconsole.log(device.manufacturer); // \"Brother\"\nconsole.log(device.model);        // \"MFC-J5730DW\"\nconsole.log(device.status);       // 'online' | 'offline' | 'busy' | 'error'\n\n// Feature access (safe query - returns undefined)\nconst maybeScan = device.getFeature<ScanFeature>('scan');\n\n// Feature access (assert - throws if not available)\nconst scanFeature = device.selectFeature<ScanFeature>('scan');\n\n// Check capabilities\ndevice.hasFeature('scan');           // true/false\ndevice.hasFeatures(['scan', 'print']); // must have ALL\ndevice.hasAnyFeature(['scan', 'print']); // must have ANY\ndevice.getFeatureTypes();            // ['scan', 'print', ...]\n```\n\n### Features\n\n#### 🖨️ ScanFeature\n\n```typescript\nconst scanFeature = device.selectFeature<ScanFeature>('scan');\nawait scanFeature.connect();\n\n// Get capabilities\nconst caps = await scanFeature.getCapabilities();\n// { resolutions: [100, 200, 300, 600], formats: ['jpeg', 'png', 'pdf'], ... }\n\n// Scan a document\nconst result = await scanFeature.scan({\n  source: 'flatbed',       // 'flatbed' | 'adf' | 'adf-duplex'\n  resolution: 300,         // DPI\n  colorMode: 'color',      // 'color' | 'grayscale' | 'blackwhite'\n  format: 'jpeg',          // 'jpeg' | 'png' | 'pdf' | 'tiff'\n  quality: 85,             // JPEG quality (1-100)\n  area: { x: 0, y: 0, width: 210, height: 297 }, // mm\n});\n\n// result.data is a Buffer containing the scanned image\nawait fs.writeFile('scan.jpg', result.data);\n\n// ADF scans expose every returned document. `data` remains the first document\n// for backwards compatibility. Use `pages` to avoid dropping later sheets/sides.\nconsole.log(`Scanner returned ${result.pageCount} document(s)`);\nfor (const [index, page] of (result.pages ?? []).entries()) {\n  await fs.writeFile(`scan-${index + 1}.jpg`, page.data);\n}\n```\n\nIf a scanner times out or cancels after transferring one or more documents,\n`EsclScanError.partialResult` contains those documents with `isComplete: false`.\nCallers can persist `partialResult.pages` before deciding whether to retry.\nSome scanner firmware accepts eSCL settings but returns a different format or\nresolution. Check `result.settingsWarnings` before treating the requested scan\nquality as confirmed.\n\nBrother devices discovered on TCP 54921 use the native scan transport before\neSCL. It explicitly selects `AUTO` for ADF input, controls simplex/duplex, and\nrejects a lease when the device does not grant the requested resolution. The\nnative transport currently returns color JPEG pages; unsupported output formats\nor color modes fail explicitly instead of being silently substituted.\nDuplex-capable scanners default to `adf-duplex`; callers can still request\n`adf` or `flatbed` explicitly. The transport preserves every returned side.\nBlank-page classification and removal belong in consuming applications, where\ndocument-specific review rules can be applied.\n\n#### 📄 PrintFeature\n\n`PrintFeature.print()` submits jobs through IPP, the print transport used by\nAirPrint. It reads printer capabilities, submits jobs, reports job state, and\nsupports cancellation. Network discovery can also identify an open\nJetDirect/raw port at TCP 9100, but JetDirect print execution is not implemented.\n\n```typescript\nconst printFeature = device.selectFeature<PrintFeature>('print');\nawait printFeature.connect();\n\n// Get printer capabilities\nconst caps = await printFeature.getCapabilities();\nconsole.log(caps.airPrintSupported, caps.documentFormats, caps.resolutions);\n\n// Print a document\nconst job = await printFeature.print(pdfBuffer, {\n  copies: 2,\n  mediaSize: 'iso_a4_210x297mm',\n  sides: 'two-sided-long-edge',\n  quality: 'high',\n  colorMode: 'color',\n  jobName: 'My Document',\n});\n\n// Get current job information\nconst jobInfo = await printFeature.getJobInfo(job.id);\n```\n\n#### 📁 SMB Share Provider\n\n`DeviceManager.provideSmbShares()` hosts one or more local folders with\n`@push.rocks/smartsamba`. The provider waits until a new file has stopped\nchanging before it emits `document:arrived`, which keeps consumers from opening\na scan while the sending device is still writing it.\n\n```typescript\nimport { DeviceManager } from '@ecobridge.xyz/devicemanager';\n\nconst manager = new DeviceManager({ autoDiscovery: false });\nconst provider = await manager.provideSmbShares({\n  host: '0.0.0.0',\n  port: 445,\n  users: [{\n    username: 'scanner',\n    password: process.env.SMB_SCANNER_PASSWORD!,\n  }],\n  shares: [{\n    name: 'scans',\n    path: '/srv/device-scans',\n    users: [{ username: 'scanner', access: 'readWrite' }],\n  }],\n  settleTimeMs: 1000,\n});\n\nconst subscription = provider.documents$.subscribe((document) => {\n  console.log(`New document: ${document.shareName}/${document.relativePath}`);\n});\n\n// The same event is forwarded through the manager as smb:document:arrived.\nconst managerSubscription = manager.events$.subscribe((event) => {\n  if (event.name === 'smb:document:arrived') {\n    console.log(event.args[0]);\n  }\n});\n\n// Later:\nsubscription.unsubscribe();\nmanagerSubscription.unsubscribe();\nawait manager.shutdown();\n```\n\nThe provider recursively monitors every configured share. It also emits\n`document:changed`, `document:removed`, `started`, `stopped`, and\n`provider:error`. Existing files are treated as the startup baseline unless\n`emitExistingDocuments` is enabled.\n\nBinding SMB's standard TCP port 445 can require elevated privileges. Published\nsmartsamba 0.3.1 packages support signed SMB 2.0.2 and 2.1 clients and include\nLinux amd64 and arm64 engines. Other platforms can provide a locally built engine\nthrough `SMARTSAMBA_RUST_BINARY`.\nDo not store SMB passwords in device metadata or log provider options.\n\n#### 🔊 PlaybackFeature & VolumeFeature\n\n`PlaybackFeature` and `VolumeFeature` provide normalized capability and state models, backed by a protocol controller when one is available.\n\nSonos and DLNA renderers are driven over UPnP SOAP: the factories and `DeviceManager`\nattach a `UpnpPlaybackController` (AVTransport) and `UpnpVolumeController`\n(RenderingControl), so these calls reach the real device. AirPlay and Chromecast have\nno bundled protocol client, so for those the same methods only update local feature\nstate and emit events.\n\n```typescript\nconst playback = device.selectFeature<PlaybackFeature>('playback');\nconst volume = device.selectFeature<VolumeFeature>('volume');\nawait playback.connect(); // primes cached state from the device\n\n// Transport control — real AVTransport SOAP calls on Sonos/DLNA\nawait playback.play('http://example.com/audio.mp3');\nawait playback.pause();\nawait playback.stop();\nawait playback.seek(120); // seconds, sent as a REL_TIME target\n\n// Live status, read back from the device\nconst status = await playback.getPlaybackStatus();\n// { state: 'playing', position: 65, duration: 260, track: { title, artist, album, ... } }\n\n// Volume — real RenderingControl SOAP calls on Sonos/DLNA\nawait volume.setVolume(50);    // 0-100, clamped to the feature range\nawait volume.setMute(true);\nawait volume.toggleMute();\nconst level = await volume.getVolume();\n```\n\nA failed command rejects rather than reporting a false success, and the cached state is\nleft untouched when the device refuses.\n\nDrive the protocol directly when you do not need the device model:\n\n```typescript\nimport { UpnpPlaybackController, UpnpVolumeController } from '@ecobridge.xyz/devicemanager';\n\nconst playback = UpnpPlaybackController.forSonos('192.168.1.52');\nawait playback.play();\n\nconst volume = UpnpVolumeController.forSonos('192.168.1.52');\nawait volume.setVolume(35);\n```\n\nSonos grouping and zones (`ZoneGroupTopology`, `GroupRenderingControl`), the queue,\nfavorites, alarms and EQ are not implemented, and there is no GENA event subscription —\nstatus is read on demand rather than pushed.\n\n#### 📢 Announcements\n\n`SpeakerAnnouncer` plays a short clip on a speaker and restores whatever it interrupted.\nA UPnP renderer fetches audio rather than accepting a push, so the clip is published on a\nlocal HTTP server (`ClipServer`) and the speaker is pointed at that URL.\n\n```typescript\nimport { SpeakerAnnouncer } from '@ecobridge.xyz/devicemanager';\n\nconst announcer = new SpeakerAnnouncer();\nawait announcer.start();\n\nawait announcer.announceOnSonos('192.168.1.52', { data: wavBuffer }, { volume: 35 });\n\nawait announcer.stop();\n```\n\nThe announcement snapshots the transport URI, position and volume, plays the clip, then\nputs everything back — resuming only if the speaker was playing beforehand. Restore also\nruns when the announcement fails, so a broken clip never strands the speaker on a dead URL\nat announcement volume.\n\nNo speech engine is bundled. Supply any synthesizer through the `ISpeechSynthesizer`\ninterface to use `announceText()`:\n\n```typescript\nconst announcer = new SpeakerAnnouncer({\n  synthesizer: {\n    synthesize: async (text) => ({ data: await myTts(text), contentType: 'audio/wav' }),\n  },\n});\nawait announcer.announceText('192.168.1.52', 'Front door open');\n```\n\nAudio must be something the renderer decodes; 44.1 kHz mono 16-bit WAV is a safe default.\n`ClipServer` sets `Content-Type` and `Content-Length` and answers `HEAD` and `Range`\nrequests, though Sonos issues neither for short clips.\n\nAnnouncements must target the group coordinator. Bonded satellites — home-theatre\nsurrounds and subs — have no transport of their own and reject transport commands, while\nstill accepting volume changes.\n\n#### 🔋 PowerFeature (UPS)\n\n```typescript\nconst power = device.selectFeature<PowerFeature>('power');\nawait power.connect();\n\n// Get UPS status\nconst status = await power.getStatus();\n// 'online' | 'onbattery' | 'lowbattery' | ... | 'unknown'\n\n// Get battery info\nconst battery = await power.getBatteryInfo();\n// { charge: 0, runtime: 0 } until a protocol-specific implementation updates it\n\n// Run a battery test when supported\nif (power.supportsTest) {\n  await power.testBattery();\n}\n```\n\n#### 🏠 Smart Home Features\n\n```typescript\n// Light control\nconst light = device.selectFeature<LightFeature>('light');\nawait light.turnOn();\nawait light.setBrightness(80); // 0-255\nawait light.setRgbColor(255, 100, 50);\nawait light.setColorTemp(4000); // Kelvin\n\n// Switch control\nconst switch_ = device.selectFeature<SwitchFeature>('switch');\nawait switch_.turnOn();\nawait switch_.turnOff();\nawait switch_.toggle();\n\n// Climate control\nconst climate = device.selectFeature<ClimateFeature>('climate');\nawait climate.setTargetTemp(22);\nawait climate.setHvacMode('heat'); // 'heat' | 'cool' | 'auto' | 'off'\n\n// Sensor reading\nconst sensor = device.selectFeature<SensorFeature>('sensor');\nconst reading = await sensor.refreshState();\n// { value: 22.5, numericValue: 22.5, unit: '°C', lastUpdated: Date }\n\n// Cached accessors reflect the most recently fetched or externally updated state\nconsole.log(sensor.value, sensor.numericValue, sensor.unit, sensor.lastUpdated);\n```\n\n### Protocol Direct Access\n\nFor advanced use cases, you can access protocols directly:\n\n```typescript\nimport { EsclProtocol, IppProtocol, SnmpProtocol } from '@ecobridge.xyz/devicemanager';\n\n// Direct eSCL (AirScan) access\nconst escl = new EsclProtocol('192.168.1.100', 80, false);\nconst caps = await escl.getCapabilities();\nconst result = await escl.scan({ source: 'flatbed', resolution: 300 });\n\n// Direct IPP access\nconst ipp = new IppProtocol('192.168.1.100', 631, '/ipp/print');\nconst printerAttrs = await ipp.getPrinterAttributes();\n\n// Direct SNMP access\nconst snmp = new SnmpProtocol('192.168.1.100', { community: 'public' });\nconst sysDescr = await snmp.get('1.3.6.1.2.1.1.1.0');\n```\n\n### Home Assistant Integration\n\n```typescript\nimport { HomeAssistantProtocol, HomeAssistantDiscovery } from '@ecobridge.xyz/devicemanager';\n\n// Connect to Home Assistant\nconst ha = new HomeAssistantProtocol({\n  host: 'homeassistant.local',\n  port: 8123,\n  token: 'your_long_lived_access_token',\n});\n\nawait ha.connect();\n\n// Get all entities\nconst entities = await ha.getStates();\n\n// Control a light\nawait ha.callService(\n  'light',\n  'turn_on',\n  { entity_id: 'light.living_room' },\n  { brightness: 200 }\n);\n\n// Subscribe to state changes\nawait ha.subscribeToStateChanges();\nha.on('state:changed', (event) => {\n  console.log(`${event.entity_id}: ${event.new_state?.state ?? 'removed'}`);\n});\n\n// Auto-discover Home Assistant instances via mDNS\nconst discovery = new HomeAssistantDiscovery();\ndiscovery.on('instance:found', (instance) => {\n  console.log(`Found HA at ${instance.host}:${instance.port}`);\n});\nawait discovery.startMdnsDiscovery();\n```\n\n### Helper Utilities\n\n```typescript\nimport {\n  withRetry,\n  isValidIp,\n  cidrToIps,\n  getLocalSubnet,\n} from '@ecobridge.xyz/devicemanager';\n\n// Retry with exponential backoff\nconst result = await withRetry(\n  () => someFlakeyOperation(),\n  { maxRetries: 3, baseDelay: 1000, multiplier: 2 }\n);\n\n// IP utilities\nisValidIp('192.168.1.1');           // true\ncidrToIps('192.168.1.0/30');        // ['192.168.1.1', '192.168.1.2']\ngetLocalSubnet();                    // '192.168.1.0/24'\n```\n\n## 🔍 Discovery Methods\n\nThe library supports multiple discovery mechanisms:\n\n| Method | Protocol | Use Case |\n|--------|----------|----------|\n| `discoverScanners()` | eSCL, SANE | Find network scanners |\n| `discoverPrinters()` | IPP | Find network printers |\n| `scanNetwork()` | All | Comprehensive subnet scan |\n| `startDiscovery()` | mDNS, SSDP | Continuous auto-discovery |\n\n### mDNS Service Types\n\n```typescript\nimport { SERVICE_TYPES } from '@ecobridge.xyz/devicemanager';\n\n// Keys: ESCL, ESCL_SECURE, SANE, IPP, IPPS, PDL,\n// AIRPLAY, RAOP, SONOS, GOOGLECAST, SPOTIFY\n```\n\n### SSDP Service Types\n\n```typescript\nimport { SSDP_SERVICE_TYPES } from '@ecobridge.xyz/devicemanager';\n\n// Root, DLNA media renderer/server, Sonos ZonePlayer,\n// UPnP basic device and internet gateway service types\n```\n\n`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.\n\n## 🎯 Feature Types\n\n```typescript\ntype TFeatureType =\n  | 'scan'       // Document scanning\n  | 'print'      // Document printing\n  | 'fax'        // Fax send/receive\n  | 'copy'       // Copy (scan + print)\n  | 'playback'   // Media playback\n  | 'volume'     // Volume control\n  | 'power'      // Power/UPS status\n  | 'snmp'       // SNMP queries\n  | 'dlna-render'// DLNA renderer\n  | 'dlna-serve' // DLNA server\n  | 'light'      // Smart lights\n  | 'climate'    // HVAC/thermostats\n  | 'sensor'     // Sensors\n  | 'camera'     // Cameras\n  | 'cover'      // Blinds, garage doors\n  | 'switch'     // Smart switches\n  | 'lock'       // Smart locks\n  | 'fan'        // Fans\n  ;\n```\n\n## 🔧 Advanced Usage\n\n### Custom Device Creation\n\nUse the factory functions for creating devices with specific features:\n\n```typescript\nimport { createScanner, createPrinter, createSpeaker } from '@ecobridge.xyz/devicemanager';\n\n// Create a scanner device manually\nconst scanner = createScanner({\n  id: 'my-scanner',\n  name: 'Office Scanner',\n  address: '192.168.1.50',\n  port: 80,\n  protocol: 'escl',\n  txtRecords: {},\n});\n\n// Create a printer device\nconst printer = createPrinter({\n  id: 'my-printer',\n  name: 'Office Printer',\n  address: '192.168.1.51',\n  port: 631,\n  txtRecords: { rp: '/ipp/print' },\n});\n\n// Create a Sonos speaker with live UPnP playback and volume control attached\nconst speaker = createSpeaker({\n  id: 'living-room-sonos',\n  name: 'Living Room',\n  address: '192.168.1.52',\n  port: 1400,\n  protocol: 'sonos',\n});\n```\n\n### Smart Home Factory Functions\n\nThe 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.\n\n### Event Handling\n\n`DeviceManager`, `UniversalDevice`, every `Feature`, and host-side providers\nremain Node `EventEmitter` instances and additionally expose `events$`. Each\nobservable item contains the event `name`, its `args`, and `emittedAt`.\n\n```typescript\nconst manager = new DeviceManager();\n\nconst subscription = manager.events$.subscribe((event) => {\n  console.log(event.name, event.args, event.emittedAt);\n});\n\n// Discovery events\nmanager.on('device:found', ({ device, featureType }) => {\n  console.log(`Found ${device.name} with ${featureType} capability`);\n});\n\nmanager.on('device:lost', (address) => {\n  console.log(`Device at ${address} went offline`);\n});\n\n// Network scan progress\nmanager.on('network:progress', (progress) => {\n  console.log(`Scanning: ${progress.percentage}% - Found ${progress.devicesFound} devices`);\n});\n\n// Device events\nconst device = manager.selectDevice({ address: '192.168.1.100' });\ndevice.on('status:changed', ({ oldStatus, newStatus }) => {\n  console.log(`Status: ${oldStatus} → ${newStatus}`);\n});\ndevice.on('feature:connected', (featureType) => {\n  console.log(`Feature ${featureType} connected`);\n});\n\nsubscription.unsubscribe();\n```\n\n### Error Handling\n\nThe library uses a fail-fast approach with clear error messages:\n\n```typescript\ntry {\n  // Throws if no device matches\n  const device = manager.selectDevice({ address: '192.168.1.999' });\n} catch (err) {\n  // \"No device found matching: {\\\"address\\\":\\\"192.168.1.999\\\"}\"\n}\n\ntry {\n  // Throws if device doesn't have the feature\n  const printFeature = device.selectFeature<PrintFeature>('print');\n} catch (err) {\n  // \"Device 'Brother Scanner' does not have feature 'print'\"\n}\n\n// Safe alternatives that don't throw\nconst devices = manager.getDevices({ address: '192.168.1.999' }); // []\nconst maybePrint = device.getFeature<PrintFeature>('print'); // undefined\n```\n\n## 📋 Requirements\n\n- **Node.js** 18+ (native `fetch` support required)\n- **TypeScript** 5.0+ (recommended)\n- **Network access** to target devices\n\n## 🙏 Credits\n\nBuilt with ❤️ using:\n- [bonjour-service](https://github.com/onlxltd/bonjour-service) - mDNS discovery\n- [Node.js networking and fetch APIs](https://nodejs.org/api/) - SSDP, IPP, eSCL, UPnP, and active network discovery\n- [net-snmp](https://github.com/markabrahams/node-net-snmp) - SNMP protocol\n- [ws](https://github.com/websockets/ws) - Home Assistant WebSocket transport\n\n## License and Legal Information\n\nThis 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.\n\n**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.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.\n\nUse of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.\n\n### Company Information\n\nTask Venture Capital GmbH\nRegistered at District Court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n","readmeFilename":""}