6 Commits

Author SHA1 Message Date
be993bf667 v3.0.1
Some checks failed
Default (tags) / security (push) Successful in 32s
Default (tags) / test (push) Failing after 32s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-12 09:13:22 +00:00
1cc8c48315 fix(release): add npm registries to release config and expand documentation for UniversalDevice architecture and smart-home features 2026-01-12 09:13:22 +00:00
3377053ef4 v3.0.0
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 1m44s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-10 09:23:36 +00:00
d7240696a8 BREAKING CHANGE(devicemanager): migrate tests to new UniversalDevice/feature-based API, add device factories, SNMP protocol/feature and IP helper utilities 2026-01-10 09:23:36 +00:00
2e82ec1884 v2.3.1
Some checks failed
Default (tags) / security (push) Successful in 24s
Default (tags) / test (push) Failing after 33s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-09 17:21:26 +00:00
79b05d47aa fix(readme): update README to comprehensive, TypeScript-first documentation covering installation, quick start, examples, API usage, events, error handling, requirements, credits, and legal/company information 2026-01-09 17:21:26 +00:00
7 changed files with 838 additions and 68 deletions

View File

@@ -1,5 +1,31 @@
# Changelog # Changelog
## 2026-01-12 - 3.0.1 - fix(release)
add npm registries to release config and expand documentation for UniversalDevice architecture and smart-home features
- npmextra.json: add "registries" to release configuration to publish to both Verdaccio (https://verdaccio.lossless.digital) and the npm registry
- readme.hints.md: rewritten/expanded implementation notes to describe the UniversalDevice architecture, composable features (including smart-home types like light, switch, sensor, climate, cover, lock, fan, camera), protocols, discovery, factories, interfaces, and testing guidance
- readme.md: add factory usage examples including smart-home factory functions, update txtRecords usage (rp) in examples, and small copy/emoji edits
## 2026-01-10 - 3.0.0 - BREAKING CHANGE(devicemanager)
migrate tests to new UniversalDevice/feature-based API, add device factories, SNMP protocol/feature and IP helper utilities
- Replace protocol-specific device classes (Scanner, Printer) with UniversalDevice and feature objects (ScanFeature, PrintFeature, PlaybackFeature, VolumeFeature, PowerFeature, SnmpFeature)
- Add device factory functions: createScanner, createPrinter, createSpeaker, createUpsDevice
- Add DeviceManager.getDevices selector and updated selectDevice behavior (throws when no match)
- Expose SnmpProtocol and other protocol implementations
- Introduce IP helper utilities: isValidIp, cidrToIps, getLocalSubnet
- Update tests and logging to use feature-based APIs and factories (selectFeature/getFeature, hasFeature, featureCount)
## 2026-01-09 - 2.3.1 - fix(readme)
update README to comprehensive, TypeScript-first documentation covering installation, quick start, examples, API usage, events, error handling, requirements, credits, and legal/company information
- Rewrote readme.md with ~590 additional lines to provide a full usage guide and examples
- Added installation instructions for pnpm, npm, and yarn and badges for npm and license
- Documented OOP usage pattern (Discovery → Selection → Feature → Operation), event handling, and error handling examples
- Clarified requirements (Node.js 18+, TypeScript 5.0+), credits, license, trademark and company contact information
- Docs-only change — no code or API modifications
## 2026-01-09 - 2.3.0 - feat(devicemanager) ## 2026-01-09 - 2.3.0 - feat(devicemanager)
add selector-based device APIs, selectFeature helper, convenience discovery methods, and ESCL scan completion fallback add selector-based device APIs, selectFeature helper, convenience discovery methods, and ESCL scan completion fallback

View File

@@ -11,10 +11,14 @@
"projectDomain": "ecobridge.xyz" "projectDomain": "ecobridge.xyz"
}, },
"release": { "release": {
"accessLevel": "public" "accessLevel": "public",
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
]
} }
}, },
"@ship.zone/szci": { "@ship.zone/szci": {
"npmGlobalTools": [] "npmGlobalTools": []
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ecobridge.xyz/devicemanager", "name": "@ecobridge.xyz/devicemanager",
"version": "2.3.0", "version": "3.0.1",
"private": false, "private": false,
"description": "a device manager for talking to devices on network and over usb", "description": "a device manager for talking to devices on network and over usb",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",

View File

@@ -2,20 +2,20 @@
## Architecture Overview ## Architecture Overview
The device manager supports two architectures: The device manager uses a **UniversalDevice** architecture with composable features.
### Legacy Architecture (Still Supported) ### Key Concepts
- Separate device classes: `Scanner`, `Printer`, `Speaker`, `SnmpDevice`, `UpsDevice`, `DlnaRenderer`, `DlnaServer`
- Type-specific collections in DeviceManager
- Type-based queries: `getScanners()`, `getPrinters()`, `getSpeakers()`
### New Universal Device Architecture - **UniversalDevice**: A single device class that can have multiple features attached
- Single `UniversalDevice` class with composable features - **Features**: Composable capabilities (scan, print, playback, volume, power, snmp, smart home, etc.)
- Features are capabilities that can be attached to any device - **Protocols**: Low-level protocol implementations (eSCL, SANE, IPP, SNMP, NUT, UPnP, Home Assistant)
- Supports multifunction devices naturally (e.g., printer+scanner)
## Key Files ## Key Files
### Core (`ts/`)
- `devicemanager.classes.devicemanager.ts` - Main DeviceManager class with discovery and device registry
- `device/device.classes.device.ts` - UniversalDevice class with feature management
### Features (`ts/features/`) ### Features (`ts/features/`)
- `feature.abstract.ts` - Base Feature class with connection management and retry logic - `feature.abstract.ts` - Base Feature class with connection management and retry logic
- `feature.scan.ts` - Scanning via eSCL/SANE protocols - `feature.scan.ts` - Scanning via eSCL/SANE protocols
@@ -24,43 +24,92 @@ The device manager supports two architectures:
- `feature.volume.ts` - Volume control (separate from playback) - `feature.volume.ts` - Volume control (separate from playback)
- `feature.power.ts` - UPS/power monitoring via NUT/SNMP - `feature.power.ts` - UPS/power monitoring via NUT/SNMP
- `feature.snmp.ts` - SNMP queries - `feature.snmp.ts` - SNMP queries
- `feature.light.ts` - Smart light control
- `feature.switch.ts` - Smart switch control
- `feature.sensor.ts` - Smart sensors
- `feature.climate.ts` - Climate/HVAC control
- `feature.cover.ts` - Blinds, garage doors
- `feature.lock.ts` - Smart locks
- `feature.fan.ts` - Fan control
- `feature.camera.ts` - Camera control
### Device (`ts/device/`) ### Protocols (`ts/protocols/`)
- `device.classes.device.ts` - UniversalDevice class with feature management - `protocol.escl.ts` - eSCL/AirScan scanner protocol
- `protocol.sane.ts` - SANE network scanner protocol
- `protocol.ipp.ts` - IPP printer protocol
- `protocol.snmp.ts` - SNMP queries
- `protocol.nut.ts` - Network UPS Tools protocol
- `protocol.upnp.ts` - UPnP/SOAP client
- `protocol.upssnmp.ts` - UPS-specific SNMP
- `protocol.homeassistant.ts` - Home Assistant WebSocket API
### Discovery (`ts/discovery/`)
- `discovery.classes.mdns.ts` - mDNS discovery (Bonjour)
- `discovery.classes.ssdp.ts` - SSDP/UPnP discovery
- `discovery.classes.networkscanner.ts` - Active network scanning
- `discovery.classes.homeassistant.ts` - Home Assistant instance discovery
### Factories (`ts/factories/`)
- `index.ts` - Device factory functions for creating pre-configured devices:
- `createScanner`, `createPrinter`, `createSpeaker`, `createDlnaRenderer`
- `createSnmpDevice`, `createUpsDevice`
- Smart home: `createSmartLight`, `createSmartSwitch`, `createSmartSensor`, etc.
### Interfaces (`ts/interfaces/`) ### Interfaces (`ts/interfaces/`)
- `feature.interfaces.ts` - All feature-related types and interfaces - `feature.interfaces.ts` - All feature-related types and interfaces
- `index.ts` - Re-exports feature interfaces - `smarthome.interfaces.ts` - Smart home specific interfaces
- `homeassistant.interfaces.ts` - Home Assistant API types
- `index.ts` - Re-exports all interfaces
## Feature Types ## Feature Types
```typescript ```typescript
type TFeatureType = type TFeatureType =
| 'scan' | 'print' | 'fax' | 'copy' | 'scan' | 'print' | 'fax' | 'copy'
| 'playback' | 'volume' | 'power' | 'snmp' | 'playback' | 'volume' | 'power' | 'snmp'
| 'dlna-render' | 'dlna-serve'; | 'dlna-render' | 'dlna-serve'
| 'light' | 'climate' | 'sensor' | 'camera'
| 'cover' | 'switch' | 'lock' | 'fan';
``` ```
## DeviceManager Feature API ## DeviceManager API
```typescript ```typescript
// Query by features // Query by features
dm.getDevicesWithFeature('scan'); // Devices with scan feature dm.getDevices(); // All devices
dm.getDevices({ hasFeature: 'scan' }); // Devices with scan feature
dm.getDevices({ name: 'Brother' }); // Devices matching name
dm.getDevicesWithFeatures(['scan', 'print']); // Devices with ALL features dm.getDevicesWithFeatures(['scan', 'print']); // Devices with ALL features
dm.getDevicesWithAnyFeature(['playback', 'volume']); // Devices with ANY feature dm.getDevicesWithAnyFeature(['playback', 'volume']); // Devices with ANY feature
// Manage universal devices // Select (throws if not found)
dm.addUniversalDevice(device); dm.selectDevice({ address: '192.168.1.100' });
dm.addFeatureToDevice(deviceId, feature);
dm.removeFeatureFromDevice(deviceId, featureType); // Discovery
dm.discoverScanners('192.168.1.0/24');
dm.discoverPrinters('192.168.1.0/24');
dm.scanNetwork({ ipRange: '...', probeEscl: true, ... });
dm.startDiscovery(); // mDNS/SSDP
dm.stopDiscovery();
``` ```
## Protocol Implementations ## UniversalDevice API
- `EsclProtocol` - eSCL/AirScan scanner protocol
- `SaneProtocol` - SANE network scanner protocol
- `IppProtocol` - IPP printer protocol
- `SnmpProtocol` - SNMP queries
- `NutProtocol` - Network UPS Tools protocol
## Type Notes ```typescript
- `TScanFormat` includes 'tiff' (added for compatibility) // Feature access
- `IPrinterCapabilities` (from index.ts) has `string[]` for sides/quality device.hasFeature('scan');
- `IPrintCapabilities` (from feature.interfaces.ts) has typed arrays device.getFeature<ScanFeature>('scan'); // Returns undefined if not found
device.selectFeature<ScanFeature>('scan'); // Throws if not found
device.getFeatureTypes(); // ['scan', 'print', ...]
// Connection
await device.connect(); // Connect all features
await device.disconnect(); // Disconnect all features
```
## Testing Notes
- Test files use `@git.zone/tstest` with tap-based assertions
- Import `expect` from `@git.zone/tstest/tapbundle`
- Tests are in `test/` directory
- Run with `pnpm test` or `tstest test/test.some.ts --verbose`

625
readme.md
View File

@@ -1,5 +1,624 @@
# @ecobridge.xyz/devicemanager # @ecobridge.xyz/devicemanager
a device manager for talking to devices on network and over usb
## How to create the docs A comprehensive, TypeScript-first device manager for discovering and communicating with network devices. 🔌
To create docs run gitzone aidoc.
[![npm version](https://img.shields.io/npm/v/@ecobridge.xyz/devicemanager.svg)](https://www.npmjs.com/package/@ecobridge.xyz/devicemanager)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
## 🎯 Overview
`@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.
**Supported Device Types:**
- 🖨️ **Scanners** — eSCL (AirScan), SANE protocols
- 📄 **Printers** — IPP, JetDirect protocols
- 🔊 **Speakers** — Sonos, AirPlay, Chromecast, DLNA
- 🔋 **UPS Systems** — NUT, SNMP protocols
- 📡 **SNMP Devices** — Generic SNMP v1/v2c/v3 support
- 🏠 **Smart Home** — Home Assistant integration (lights, switches, sensors, climate, locks, fans, cameras, covers)
## Issue Reporting and Security
For 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.
## 📦 Installation
```bash
# Using pnpm (recommended)
pnpm add @ecobridge.xyz/devicemanager
# Using npm
npm install @ecobridge.xyz/devicemanager
# Using yarn
yarn add @ecobridge.xyz/devicemanager
```
## 🚀 Quick Start
### The OOP Pattern: Discovery → Selection → Feature → Operation
```typescript
import { DeviceManager, ScanFeature } from '@ecobridge.xyz/devicemanager';
import * as fs from 'fs/promises';
async function scanDocument() {
const manager = new DeviceManager();
// 1⃣ DISCOVERY - Find scanners in your network
await manager.discoverScanners('192.168.1.0/24');
// 2⃣ INSPECTION - See what's available
const scanners = manager.getDevices({ hasFeature: 'scan' });
console.log('Found scanners:', scanners.map(s => `${s.name} at ${s.address}`));
// 3⃣ SELECTION - Choose your device (explicit, no magic!)
const device = manager.selectDevice({ address: '192.168.1.100' });
// 4⃣ FEATURE ACCESS - Get the capability you need
const scanFeature = device.selectFeature<ScanFeature>('scan');
// 5⃣ OPERATION - Do the thing!
await scanFeature.connect();
const result = await scanFeature.scan({
source: 'flatbed',
resolution: 300,
colorMode: 'color',
format: 'jpeg',
});
await fs.writeFile('scan.jpg', result.data);
console.log(`Saved: scan.jpg (${result.data.length} bytes)`);
await manager.shutdown();
}
scanDocument();
```
## 🏗️ Architecture
The library follows a clean, composable architecture:
```
┌─────────────────────────────────────────────────────────────┐
│ DeviceManager │
│ • Discovery (mDNS, SSDP, Network Scanning) │
│ • Device Registry (by IP, deduplication) │
│ • Device Selection (query & assert patterns) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ UniversalDevice │
│ • Represents any network device │
│ • Composable features (scan, print, volume, etc.) │
│ • Connection lifecycle management │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Features │
│ ScanFeature │ PrintFeature │ PlaybackFeature │ VolumeFeature
│ PowerFeature │ SnmpFeature │ LightFeature │ SwitchFeature │
│ SensorFeature │ ClimateFeature │ CameraFeature │ ... │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Protocols │
│ eSCL │ SANE │ IPP │ SNMP │ NUT │ UPnP/SOAP │ Home Assistant│
└─────────────────────────────────────────────────────────────┘
```
## 📖 API Reference
### DeviceManager
The central orchestrator for device discovery and management.
```typescript
const manager = new DeviceManager({
autoDiscovery: true, // Enable mDNS/SSDP auto-discovery
discoveryTimeout: 10000, // Discovery timeout in ms
enableRetry: true, // Enable retry with exponential backoff
maxRetries: 5, // Maximum retry attempts
});
```
#### Discovery Methods
```typescript
// Focused scanner discovery
const scanners = await manager.discoverScanners('192.168.1.0/24', {
timeout: 3000,
concurrency: 50,
});
// Focused printer discovery
const printers = await manager.discoverPrinters('192.168.1.0/24');
// General network scan (all device types)
const devices = await manager.scanNetwork({
ipRange: '192.168.1.0/24',
probeEscl: true,
probeIpp: true,
probeSane: true,
probeAirplay: true,
probeSonos: true,
probeChromecast: true,
});
// mDNS/SSDP continuous discovery
await manager.startDiscovery();
manager.on('device:found', ({ device, featureType }) => {
console.log(`Found: ${device.name}`);
});
await manager.stopDiscovery();
```
#### Device Selection
```typescript
// Query pattern (returns array, may be empty)
const allDevices = manager.getDevices();
const scanners = manager.getDevices({ hasFeature: 'scan' });
const brotherDevices = manager.getDevices({ name: 'Brother' });
// Assert pattern (returns single device, throws if not found)
const device = manager.selectDevice({ address: '192.168.1.100' });
const scanner = manager.selectDevice({ name: 'Brother', hasFeature: 'scan' });
```
#### IDeviceSelector Interface
```typescript
interface IDeviceSelector {
id?: string; // Exact match on device ID
address?: string; // Exact match on IP address
name?: string; // Partial match (case-insensitive)
model?: string; // Partial match (case-insensitive)
manufacturer?: string; // Partial match (case-insensitive)
hasFeature?: TFeatureType; // Must have this feature
hasFeatures?: TFeatureType[]; // Must have ALL features
hasAnyFeature?: TFeatureType[]; // Must have ANY feature
}
```
### UniversalDevice
Represents any network device with composable features.
```typescript
const device = manager.selectDevice({ address: '192.168.1.100' });
// Device properties
console.log(device.name); // "Brother MFC-J5730DW"
console.log(device.address); // "192.168.1.100"
console.log(device.manufacturer); // "Brother"
console.log(device.model); // "MFC-J5730DW"
console.log(device.status); // 'online' | 'offline' | 'busy' | 'error'
// Feature access (safe query - returns undefined)
const maybeScan = device.getFeature<ScanFeature>('scan');
// Feature access (assert - throws if not available)
const scanFeature = device.selectFeature<ScanFeature>('scan');
// Check capabilities
device.hasFeature('scan'); // true/false
device.hasFeatures(['scan', 'print']); // must have ALL
device.hasAnyFeature(['scan', 'print']); // must have ANY
device.getFeatureTypes(); // ['scan', 'print', ...]
```
### Features
#### 🖨️ ScanFeature
```typescript
const scanFeature = device.selectFeature<ScanFeature>('scan');
await scanFeature.connect();
// Get capabilities
const caps = await scanFeature.getCapabilities();
// { resolutions: [100, 200, 300, 600], formats: ['jpeg', 'png', 'pdf'], ... }
// Scan a document
const result = await scanFeature.scan({
source: 'flatbed', // 'flatbed' | 'adf' | 'adf-duplex'
resolution: 300, // DPI
colorMode: 'color', // 'color' | 'grayscale' | 'blackwhite'
format: 'jpeg', // 'jpeg' | 'png' | 'pdf' | 'tiff'
quality: 85, // JPEG quality (1-100)
area: { x: 0, y: 0, width: 210, height: 297 }, // mm
});
// result.data is a Buffer containing the scanned image
await fs.writeFile('scan.jpg', result.data);
```
#### 📄 PrintFeature
```typescript
const printFeature = device.selectFeature<PrintFeature>('print');
await printFeature.connect();
// Get printer capabilities
const caps = await printFeature.getCapabilities();
// Print a document
const job = await printFeature.print(pdfBuffer, {
copies: 2,
mediaSize: 'iso_a4_210x297mm',
sides: 'two-sided-long-edge',
quality: 'high',
colorMode: 'color',
jobName: 'My Document',
});
// Check job status
const status = await printFeature.getJobStatus(job.id);
```
#### 🔊 PlaybackFeature & VolumeFeature
```typescript
const playback = device.selectFeature<PlaybackFeature>('playback');
const volume = device.selectFeature<VolumeFeature>('volume');
await playback.connect();
// Control playback
await playback.play('http://example.com/audio.mp3');
await playback.pause();
await playback.stop();
await playback.seek(120); // seconds
// Get playback status
const status = await playback.getStatus();
// { state: 'playing', position: 45, duration: 180, track: { title: '...' } }
// Control volume
await volume.setVolume(50); // 0-100
await volume.mute();
await volume.unmute();
const level = await volume.getVolume();
```
#### 🔋 PowerFeature (UPS)
```typescript
const power = device.selectFeature<PowerFeature>('power');
await power.connect();
// Get UPS status
const status = await power.getStatus();
// { status: 'online', battery: { charge: 100, runtime: 1800, voltage: 13.8 }, ... }
// Get battery info
const battery = await power.getBatteryInfo();
// { charge: 95, runtime: 1500, health: 'good' }
// Run self-test
await power.runTest();
```
#### 🏠 Smart Home Features
```typescript
// Light control
const light = device.selectFeature<LightFeature>('light');
await light.turnOn();
await light.setBrightness(80);
await light.setColor({ r: 255, g: 100, b: 50 });
await light.setColorTemperature(4000); // Kelvin
// Switch control
const switch_ = device.selectFeature<SwitchFeature>('switch');
await switch_.turnOn();
await switch_.turnOff();
await switch_.toggle();
// Climate control
const climate = device.selectFeature<ClimateFeature>('climate');
await climate.setTargetTemperature(22);
await climate.setMode('heat'); // 'heat' | 'cool' | 'auto' | 'off'
// Sensor reading
const sensor = device.selectFeature<SensorFeature>('sensor');
const reading = await sensor.getState();
// { temperature: 22.5, humidity: 45, battery: 85 }
```
### Protocol Direct Access
For advanced use cases, you can access protocols directly:
```typescript
import { EsclProtocol, IppProtocol, SnmpProtocol } from '@ecobridge.xyz/devicemanager';
// Direct eSCL (AirScan) access
const escl = new EsclProtocol('192.168.1.100', 80, false);
const caps = await escl.getCapabilities();
const result = await escl.scan({ source: 'flatbed', resolution: 300 });
// Direct IPP access
const ipp = new IppProtocol('ipp://192.168.1.100:631/ipp/print');
const printerAttrs = await ipp.getPrinterAttributes();
// Direct SNMP access
const snmp = new SnmpProtocol('192.168.1.100', { community: 'public' });
const sysDescr = await snmp.get('1.3.6.1.2.1.1.1.0');
```
### Home Assistant Integration
```typescript
import { HomeAssistantProtocol, HomeAssistantDiscovery } from '@ecobridge.xyz/devicemanager';
// Connect to Home Assistant
const ha = new HomeAssistantProtocol({
host: 'homeassistant.local',
port: 8123,
accessToken: 'your_long_lived_access_token',
});
await ha.connect();
// Get all entities
const entities = await ha.getStates();
// Control a light
await ha.callService('light', 'turn_on', {
entity_id: 'light.living_room',
brightness: 200,
});
// Subscribe to state changes
ha.on('state_changed', (event) => {
console.log(`${event.entity_id}: ${event.new_state.state}`);
});
// Auto-discover Home Assistant instances via mDNS
const discovery = new HomeAssistantDiscovery();
discovery.on('found', (instance) => {
console.log(`Found HA at ${instance.address}:${instance.port}`);
});
await discovery.start();
```
### Helper Utilities
```typescript
import {
withRetry,
isValidIp,
cidrToIps,
getLocalSubnet,
} from '@ecobridge.xyz/devicemanager';
// Retry with exponential backoff
const result = await withRetry(
() => someFlakeyOperation(),
{ maxRetries: 3, baseDelay: 1000, multiplier: 2 }
);
// IP utilities
isValidIp('192.168.1.1'); // true
cidrToIps('192.168.1.0/30'); // ['192.168.1.0', '192.168.1.1', ...]
getLocalSubnet(); // '192.168.1.0/24'
```
## 🔍 Discovery Methods
The library supports multiple discovery mechanisms:
| Method | Protocol | Use Case |
|--------|----------|----------|
| `discoverScanners()` | eSCL, SANE | Find network scanners |
| `discoverPrinters()` | IPP | Find network printers |
| `scanNetwork()` | All | Comprehensive subnet scan |
| `startDiscovery()` | mDNS, SSDP | Continuous auto-discovery |
### mDNS Service Types
```typescript
import { SERVICE_TYPES } from '@ecobridge.xyz/devicemanager';
// Available: escl, ipp, ipp-tls, airplay, raop,
// googlecast, sonos, sane, http, https, printer
```
### SSDP Service Types
```typescript
import { SSDP_SERVICE_TYPES } from '@ecobridge.xyz/devicemanager';
// Available: all, rootdevice, mediaRenderer, mediaServer,
// contentDirectory, avtransport, renderingControl,
// connectionManager, zonePlayer
```
## 🎯 Feature Types
```typescript
type TFeatureType =
| 'scan' // Document scanning
| 'print' // Document printing
| 'fax' // Fax send/receive
| 'copy' // Copy (scan + print)
| 'playback' // Media playback
| 'volume' // Volume control
| 'power' // Power/UPS status
| 'snmp' // SNMP queries
| 'dlna-render'// DLNA renderer
| 'dlna-serve' // DLNA server
| 'light' // Smart lights
| 'climate' // HVAC/thermostats
| 'sensor' // Sensors
| 'camera' // Cameras
| 'cover' // Blinds, garage doors
| 'switch' // Smart switches
| 'lock' // Smart locks
| 'fan' // Fans
;
```
## 🔧 Advanced Usage
### Custom Device Creation
Use the factory functions for creating devices with specific features:
```typescript
import { createScanner, createPrinter, createSpeaker } from '@ecobridge.xyz/devicemanager';
// Create a scanner device manually
const scanner = createScanner({
id: 'my-scanner',
name: 'Office Scanner',
address: '192.168.1.50',
port: 80,
protocol: 'escl',
txtRecords: {},
});
// Create a printer device
const printer = createPrinter({
id: 'my-printer',
name: 'Office Printer',
address: '192.168.1.51',
port: 631,
txtRecords: { rp: '/ipp/print' },
});
// Create a Sonos speaker
const speaker = createSpeaker({
id: 'living-room-sonos',
name: 'Living Room',
address: '192.168.1.52',
port: 1400,
protocol: 'sonos',
});
```
### Smart Home Factory Functions
```typescript
import {
createSmartLight,
createSmartSwitch,
createSmartSensor,
createSmartClimate,
createSmartCover,
createSmartLock,
createSmartFan,
createSmartCamera,
} from '@ecobridge.xyz/devicemanager';
// Create devices with Home Assistant integration
const light = createSmartLight({
id: 'living-room-light',
name: 'Living Room Light',
address: 'homeassistant.local',
port: 8123,
entityId: 'light.living_room',
protocol: 'home-assistant',
protocolClient: haClient, // Your HomeAssistantProtocol instance
capabilities: {
supportsBrightness: true,
supportsColorTemp: true,
supportsRgb: true,
},
});
```
### Event Handling
```typescript
const manager = new DeviceManager();
// Discovery events
manager.on('device:found', ({ device, featureType }) => {
console.log(`Found ${device.name} with ${featureType} capability`);
});
manager.on('device:lost', (address) => {
console.log(`Device at ${address} went offline`);
});
// Network scan progress
manager.on('network:progress', (progress) => {
console.log(`Scanning: ${progress.percentage}% - Found ${progress.devicesFound} devices`);
});
// Device events
const device = manager.selectDevice({ address: '192.168.1.100' });
device.on('status:changed', ({ oldStatus, newStatus }) => {
console.log(`Status: ${oldStatus}${newStatus}`);
});
device.on('feature:connected', (featureType) => {
console.log(`Feature ${featureType} connected`);
});
```
### Error Handling
The library uses a fail-fast approach with clear error messages:
```typescript
try {
// Throws if no device matches
const device = manager.selectDevice({ address: '192.168.1.999' });
} catch (err) {
// "No device found matching: {\"address\":\"192.168.1.999\"}"
}
try {
// Throws if device doesn't have the feature
const printFeature = device.selectFeature<PrintFeature>('print');
} catch (err) {
// "Device 'Brother Scanner' does not have feature 'print'"
}
// Safe alternatives that don't throw
const devices = manager.getDevices({ address: '192.168.1.999' }); // []
const maybePrint = device.getFeature<PrintFeature>('print'); // undefined
```
## 📋 Requirements
- **Node.js** 18+ (native `fetch` support required)
- **TypeScript** 5.0+ (recommended)
- **Network access** to target devices
## 🙏 Credits
Built with ❤️ using:
- [bonjour-service](https://github.com/onlxltd/bonjour-service) - mDNS discovery
- [node-ssdp](https://github.com/diversario/node-ssdp) - SSDP/UPnP discovery
- [net-snmp](https://github.com/markabrahams/node-net-snmp) - SNMP protocol
- [ipp](https://github.com/niclaslindstedt/ipp) - IPP printing protocol
- [sonos](https://github.com/bencevans/node-sonos) - Sonos control
- [castv2-client](https://github.com/thibauts/node-castv2-client) - Chromecast protocol
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
**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.
### Trademarks
This 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.
Use 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.
### Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By 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.

View File

@@ -1,26 +1,40 @@
import { expect, tap } from '@git.zone/tstest/tapbundle'; import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as devicemanager from '../ts/index.js'; import * as devicemanager from '../ts/index.js';
// Test imports // Test core exports
tap.test('should export DeviceManager', async () => { tap.test('should export DeviceManager', async () => {
expect(devicemanager.DeviceManager).toBeDefined(); expect(devicemanager.DeviceManager).toBeDefined();
expect(typeof devicemanager.DeviceManager).toEqual('function'); expect(typeof devicemanager.DeviceManager).toEqual('function');
}); });
tap.test('should export Scanner', async () => { tap.test('should export UniversalDevice', async () => {
expect(devicemanager.Scanner).toBeDefined(); expect(devicemanager.UniversalDevice).toBeDefined();
expect(typeof devicemanager.Scanner).toEqual('function'); expect(typeof devicemanager.UniversalDevice).toEqual('function');
}); });
tap.test('should export Printer', async () => { tap.test('should export Features', async () => {
expect(devicemanager.Printer).toBeDefined(); expect(devicemanager.ScanFeature).toBeDefined();
expect(typeof devicemanager.Printer).toEqual('function'); expect(devicemanager.PrintFeature).toBeDefined();
expect(devicemanager.PlaybackFeature).toBeDefined();
expect(devicemanager.VolumeFeature).toBeDefined();
expect(devicemanager.PowerFeature).toBeDefined();
expect(devicemanager.SnmpFeature).toBeDefined();
});
tap.test('should export device factories', async () => {
expect(devicemanager.createScanner).toBeDefined();
expect(devicemanager.createPrinter).toBeDefined();
expect(devicemanager.createSpeaker).toBeDefined();
expect(devicemanager.createUpsDevice).toBeDefined();
expect(typeof devicemanager.createScanner).toEqual('function');
expect(typeof devicemanager.createPrinter).toEqual('function');
}); });
tap.test('should export protocol implementations', async () => { tap.test('should export protocol implementations', async () => {
expect(devicemanager.EsclProtocol).toBeDefined(); expect(devicemanager.EsclProtocol).toBeDefined();
expect(devicemanager.SaneProtocol).toBeDefined(); expect(devicemanager.SaneProtocol).toBeDefined();
expect(devicemanager.IppProtocol).toBeDefined(); expect(devicemanager.IppProtocol).toBeDefined();
expect(devicemanager.SnmpProtocol).toBeDefined();
}); });
tap.test('should export retry helpers', async () => { tap.test('should export retry helpers', async () => {
@@ -39,6 +53,28 @@ tap.test('should create DeviceManager instance', async () => {
expect(dm.isDiscovering).toEqual(false); expect(dm.isDiscovering).toEqual(false);
expect(dm.getScanners()).toEqual([]); expect(dm.getScanners()).toEqual([]);
expect(dm.getPrinters()).toEqual([]); expect(dm.getPrinters()).toEqual([]);
expect(dm.getDevices()).toEqual([]);
});
// Test device selector
tap.test('should support device selection with IDeviceSelector', async () => {
const dm = new devicemanager.DeviceManager({
autoDiscovery: false,
});
// Empty manager should return empty array
expect(dm.getDevices({ hasFeature: 'scan' })).toEqual([]);
expect(dm.getDevices({ name: 'Test' })).toEqual([]);
// selectDevice should throw when no devices match
let error: Error | null = null;
try {
dm.selectDevice({ address: '192.168.1.100' });
} catch (e) {
error = e as Error;
}
expect(error).not.toBeNull();
expect(error?.message).toContain('No device found');
}); });
// Test retry helper // Test retry helper
@@ -114,17 +150,18 @@ tap.test('should start and stop discovery', async () => {
// Wait a bit for potential device discovery // Wait a bit for potential device discovery
await new Promise((resolve) => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, 2000));
// Log discovered devices // Log discovered devices using new API
const scanners = dm.getScanners(); const scanners = dm.getDevices({ hasFeature: 'scan' });
const printers = dm.getPrinters(); const printers = dm.getDevices({ hasFeature: 'print' });
console.log(`Discovered ${scanners.length} scanner(s) and ${printers.length} printer(s)`); console.log(`Discovered ${scanners.length} scanner(s) and ${printers.length} printer(s)`);
for (const scanner of scanners) { for (const scanner of scanners) {
console.log(` Scanner: ${scanner.name} (${scanner.address}:${scanner.port}) - ${scanner.protocol}`); const scanFeature = scanner.getFeature<devicemanager.ScanFeature>('scan');
console.log(` Scanner: ${scanner.name} (${scanner.address}) - ${scanFeature?.protocol || 'unknown'}`);
} }
for (const printer of printers) { for (const printer of printers) {
console.log(` Printer: ${printer.name} (${printer.address}:${printer.port})`); console.log(` Printer: ${printer.name} (${printer.address})`);
} }
await dm.stopDiscovery(); await dm.stopDiscovery();
@@ -134,9 +171,9 @@ tap.test('should start and stop discovery', async () => {
await dm.shutdown(); await dm.shutdown();
}); });
// Test Scanner creation from discovery info // Test Scanner creation using factory
tap.test('should create Scanner from discovery info', async () => { tap.test('should create Scanner device using factory', async () => {
const scanner = devicemanager.Scanner.fromDiscovery({ const scanner = devicemanager.createScanner({
id: 'test:scanner:1', id: 'test:scanner:1',
name: 'Test Scanner', name: 'Test Scanner',
address: '192.168.1.100', address: '192.168.1.100',
@@ -150,40 +187,75 @@ tap.test('should create Scanner from discovery info', async () => {
}, },
}); });
expect(scanner).toBeInstanceOf(devicemanager.UniversalDevice);
expect(scanner.name).toEqual('Test Scanner'); expect(scanner.name).toEqual('Test Scanner');
expect(scanner.address).toEqual('192.168.1.100'); expect(scanner.address).toEqual('192.168.1.100');
expect(scanner.port).toEqual(443); expect(scanner.hasFeature('scan')).toEqual(true);
expect(scanner.protocol).toEqual('escl');
expect(scanner.supportedFormats).toContain('jpeg'); const scanFeature = scanner.selectFeature<devicemanager.ScanFeature>('scan');
expect(scanner.supportedFormats).toContain('pdf'); expect(scanFeature).toBeInstanceOf(devicemanager.ScanFeature);
expect(scanner.supportedColorModes).toContain('color'); expect(scanFeature.protocol).toEqual('escl');
expect(scanner.supportedColorModes).toContain('grayscale');
expect(scanner.supportedSources).toContain('flatbed');
expect(scanner.supportedSources).toContain('adf');
expect(scanner.hasAdf).toEqual(true);
}); });
// Test Printer creation from discovery info // Test Printer creation using factory
tap.test('should create Printer from discovery info', async () => { tap.test('should create Printer device using factory', async () => {
const printer = devicemanager.Printer.fromDiscovery({ const printer = devicemanager.createPrinter({
id: 'test:printer:1', id: 'test:printer:1',
name: 'Test Printer', name: 'Test Printer',
address: '192.168.1.101', address: '192.168.1.101',
port: 631, port: 631,
ippPath: '/ipp/print',
txtRecords: { txtRecords: {
'ty': 'Brother HL-L2350DW', 'ty': 'Brother HL-L2350DW',
'rp': 'ipp/print',
'Color': 'T', 'Color': 'T',
'Duplex': 'T', 'Duplex': 'T',
}, },
}); });
expect(printer).toBeInstanceOf(devicemanager.UniversalDevice);
expect(printer.name).toEqual('Test Printer'); expect(printer.name).toEqual('Test Printer');
expect(printer.address).toEqual('192.168.1.101'); expect(printer.address).toEqual('192.168.1.101');
expect(printer.port).toEqual(631); expect(printer.hasFeature('print')).toEqual(true);
expect(printer.supportsColor).toEqual(true);
expect(printer.supportsDuplex).toEqual(true); const printFeature = printer.selectFeature<devicemanager.PrintFeature>('print');
expect(printer.uri).toContain('ipp://'); expect(printFeature).toBeInstanceOf(devicemanager.PrintFeature);
});
// Test UniversalDevice feature management
tap.test('should manage features on UniversalDevice', async () => {
const device = new devicemanager.UniversalDevice('192.168.1.50', 80, {
name: 'Test Device',
});
expect(device.name).toEqual('Test Device');
expect(device.address).toEqual('192.168.1.50');
expect(device.featureCount).toEqual(0);
expect(device.hasFeature('scan')).toEqual(false);
// selectFeature should throw when feature doesn't exist
let error: Error | null = null;
try {
device.selectFeature('scan');
} catch (e) {
error = e as Error;
}
expect(error).not.toBeNull();
expect(error?.message).toContain("does not have feature 'scan'");
// getFeature should return undefined
expect(device.getFeature('scan')).toBeUndefined();
});
// Test IP helpers
tap.test('should export IP helper utilities', async () => {
expect(devicemanager.isValidIp).toBeDefined();
expect(devicemanager.cidrToIps).toBeDefined();
expect(devicemanager.getLocalSubnet).toBeDefined();
// Test isValidIp
expect(devicemanager.isValidIp('192.168.1.1')).toEqual(true);
expect(devicemanager.isValidIp('invalid')).toEqual(false);
expect(devicemanager.isValidIp('256.1.1.1')).toEqual(false);
}); });
export default tap.start(); export default tap.start();

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@ecobridge.xyz/devicemanager', name: '@ecobridge.xyz/devicemanager',
version: '2.3.0', version: '3.0.1',
description: 'a device manager for talking to devices on network and over usb' description: 'a device manager for talking to devices on network and over usb'
} }