8 Commits
v2.3.1 ... main

Author SHA1 Message Date
d9029ec02b v3.1.0
Some checks failed
Default (tags) / security (push) Successful in 33s
Default (tags) / test (push) Failing after 33s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-13 21:14:30 +00:00
b89e8cbc3c feat(print): use IPP smartPrint and normalize IPP capabilities and job mapping 2026-01-13 21:14:30 +00:00
716347bac1 v3.0.2
Some checks failed
Default (tags) / security (push) Successful in 30s
Default (tags) / test (push) Failing after 32s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-12 11:00:15 +00:00
a6ee36f187 fix(devicemanager): no changes detected - nothing to commit 2026-01-12 11:00:15 +00:00
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
10 changed files with 1526 additions and 401 deletions

View File

@@ -1,5 +1,38 @@
# Changelog
## 2026-01-13 - 3.1.0 - feat(print)
use IPP smartPrint and normalize IPP capabilities and job mapping
- Use IppProtocol.smartPrint for automatic format detection/conversion when submitting print jobs.
- Normalize and map IIppJob -> IPrintJob via mapIppJobToInternal, collapsing extended IPP job states into internal states.
- Parse IIppPrinterCapabilities fields (mediaSizeSupported, mediaTypeSupported, sidesSupported, printQualitySupported, copiesSupported) and derive supportsDuplex from sidesSupported and maxCopies from copiesSupported range with a fallback.
- Map numeric IPP printQuality values (3,4,5) to internal quality strings (draft, normal, high).
- Switched calls to getPrinterAttributes/getJobAttributes and adjusted job listing to map returned IIppJob objects.
- Export new IPP types from protocols index: IIppPrinterCapabilities, IIppJob, IIppPrintOptions.
## 2026-01-12 - 3.0.2 - fix(devicemanager)
no changes detected - nothing to commit
- git diff indicates no modifications, additions, or deletions
- no files were changed in the provided diff
## 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

View File

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

View File

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

View File

@@ -2,20 +2,20 @@
## Architecture Overview
The device manager supports two architectures:
The device manager uses a **UniversalDevice** architecture with composable features.
### Legacy Architecture (Still Supported)
- Separate device classes: `Scanner`, `Printer`, `Speaker`, `SnmpDevice`, `UpsDevice`, `DlnaRenderer`, `DlnaServer`
- Type-specific collections in DeviceManager
- Type-based queries: `getScanners()`, `getPrinters()`, `getSpeakers()`
### Key Concepts
### New Universal Device Architecture
- Single `UniversalDevice` class with composable features
- Features are capabilities that can be attached to any device
- Supports multifunction devices naturally (e.g., printer+scanner)
- **UniversalDevice**: A single device class that can have multiple features attached
- **Features**: Composable capabilities (scan, print, playback, volume, power, snmp, smart home, etc.)
- **Protocols**: Low-level protocol implementations (eSCL, SANE, IPP, SNMP, NUT, UPnP, Home Assistant)
## 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/`)
- `feature.abstract.ts` - Base Feature class with connection management and retry logic
- `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.power.ts` - UPS/power monitoring via NUT/SNMP
- `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/`)
- `device.classes.device.ts` - UniversalDevice class with feature management
### Protocols (`ts/protocols/`)
- `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/`)
- `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
```typescript
type TFeatureType =
| 'scan' | 'print' | 'fax' | 'copy'
| '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
// 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.getDevicesWithAnyFeature(['playback', 'volume']); // Devices with ANY feature
// Manage universal devices
dm.addUniversalDevice(device);
dm.addFeatureToDevice(deviceId, feature);
dm.removeFeatureFromDevice(deviceId, featureType);
// Select (throws if not found)
dm.selectDevice({ address: '192.168.1.100' });
// 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
- `EsclProtocol` - eSCL/AirScan scanner protocol
- `SaneProtocol` - SANE network scanner protocol
- `IppProtocol` - IPP printer protocol
- `SnmpProtocol` - SNMP queries
- `NutProtocol` - Network UPS Tools protocol
## UniversalDevice API
## Type Notes
- `TScanFormat` includes 'tiff' (added for compatibility)
- `IPrinterCapabilities` (from index.ts) has `string[]` for sides/quality
- `IPrintCapabilities` (from feature.interfaces.ts) has typed arrays
```typescript
// Feature access
device.hasFeature('scan');
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`

View File

@@ -468,6 +468,8 @@ type TFeatureType =
### Custom Device Creation
Use the factory functions for creating devices with specific features:
```typescript
import { createScanner, createPrinter, createSpeaker } from '@ecobridge.xyz/devicemanager';
@@ -487,8 +489,7 @@ const printer = createPrinter({
name: 'Office Printer',
address: '192.168.1.51',
port: 631,
ippPath: '/ipp/print',
txtRecords: {},
txtRecords: { rp: '/ipp/print' },
});
// Create a Sonos speaker
@@ -498,7 +499,37 @@ const speaker = createSpeaker({
address: '192.168.1.52',
port: 1400,
protocol: 'sonos',
txtRecords: {},
});
```
### 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,
},
});
```
@@ -563,7 +594,7 @@ const maybePrint = device.getFeature<PrintFeature>('print'); // undefined
## 🙏 Credits
Built with love using:
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

View File

@@ -1,26 +1,40 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as devicemanager from '../ts/index.js';
// Test imports
// Test core exports
tap.test('should export DeviceManager', async () => {
expect(devicemanager.DeviceManager).toBeDefined();
expect(typeof devicemanager.DeviceManager).toEqual('function');
});
tap.test('should export Scanner', async () => {
expect(devicemanager.Scanner).toBeDefined();
expect(typeof devicemanager.Scanner).toEqual('function');
tap.test('should export UniversalDevice', async () => {
expect(devicemanager.UniversalDevice).toBeDefined();
expect(typeof devicemanager.UniversalDevice).toEqual('function');
});
tap.test('should export Printer', async () => {
expect(devicemanager.Printer).toBeDefined();
expect(typeof devicemanager.Printer).toEqual('function');
tap.test('should export Features', async () => {
expect(devicemanager.ScanFeature).toBeDefined();
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 () => {
expect(devicemanager.EsclProtocol).toBeDefined();
expect(devicemanager.SaneProtocol).toBeDefined();
expect(devicemanager.IppProtocol).toBeDefined();
expect(devicemanager.SnmpProtocol).toBeDefined();
});
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.getScanners()).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
@@ -114,17 +150,18 @@ tap.test('should start and stop discovery', async () => {
// Wait a bit for potential device discovery
await new Promise((resolve) => setTimeout(resolve, 2000));
// Log discovered devices
const scanners = dm.getScanners();
const printers = dm.getPrinters();
// Log discovered devices using new API
const scanners = dm.getDevices({ hasFeature: 'scan' });
const printers = dm.getDevices({ hasFeature: 'print' });
console.log(`Discovered ${scanners.length} scanner(s) and ${printers.length} printer(s)`);
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) {
console.log(` Printer: ${printer.name} (${printer.address}:${printer.port})`);
console.log(` Printer: ${printer.name} (${printer.address})`);
}
await dm.stopDiscovery();
@@ -134,9 +171,9 @@ tap.test('should start and stop discovery', async () => {
await dm.shutdown();
});
// Test Scanner creation from discovery info
tap.test('should create Scanner from discovery info', async () => {
const scanner = devicemanager.Scanner.fromDiscovery({
// Test Scanner creation using factory
tap.test('should create Scanner device using factory', async () => {
const scanner = devicemanager.createScanner({
id: 'test:scanner:1',
name: 'Test Scanner',
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.address).toEqual('192.168.1.100');
expect(scanner.port).toEqual(443);
expect(scanner.protocol).toEqual('escl');
expect(scanner.supportedFormats).toContain('jpeg');
expect(scanner.supportedFormats).toContain('pdf');
expect(scanner.supportedColorModes).toContain('color');
expect(scanner.supportedColorModes).toContain('grayscale');
expect(scanner.supportedSources).toContain('flatbed');
expect(scanner.supportedSources).toContain('adf');
expect(scanner.hasAdf).toEqual(true);
expect(scanner.hasFeature('scan')).toEqual(true);
const scanFeature = scanner.selectFeature<devicemanager.ScanFeature>('scan');
expect(scanFeature).toBeInstanceOf(devicemanager.ScanFeature);
expect(scanFeature.protocol).toEqual('escl');
});
// Test Printer creation from discovery info
tap.test('should create Printer from discovery info', async () => {
const printer = devicemanager.Printer.fromDiscovery({
// Test Printer creation using factory
tap.test('should create Printer device using factory', async () => {
const printer = devicemanager.createPrinter({
id: 'test:printer:1',
name: 'Test Printer',
address: '192.168.1.101',
port: 631,
ippPath: '/ipp/print',
txtRecords: {
'ty': 'Brother HL-L2350DW',
'rp': 'ipp/print',
'Color': 'T',
'Duplex': 'T',
},
});
expect(printer).toBeInstanceOf(devicemanager.UniversalDevice);
expect(printer.name).toEqual('Test Printer');
expect(printer.address).toEqual('192.168.1.101');
expect(printer.port).toEqual(631);
expect(printer.supportsColor).toEqual(true);
expect(printer.supportsDuplex).toEqual(true);
expect(printer.uri).toContain('ipp://');
expect(printer.hasFeature('print')).toEqual(true);
const printFeature = printer.selectFeature<devicemanager.PrintFeature>('print');
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();

View File

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

View File

@@ -4,7 +4,7 @@
*/
import { Feature, type TDeviceReference } from './feature.abstract.js';
import { IppProtocol } from '../protocols/index.js';
import { IppProtocol, type IIppPrinterCapabilities, type IIppJob } from '../protocols/index.js';
import type {
TPrintProtocol,
TPrintSides,
@@ -16,7 +16,6 @@ import type {
IPrintFeatureInfo,
IFeatureOptions,
} from '../interfaces/feature.interfaces.js';
import type { IPrinterCapabilities } from '../interfaces/index.js';
/**
* Options for creating a PrintFeature
@@ -101,7 +100,7 @@ export class PrintFeature extends Feature {
this.ippClient = new IppProtocol(address, port, path);
// Verify connection by getting printer attributes
const attrs = await this.ippClient.getAttributes();
const attrs = await this.ippClient.getPrinterAttributes();
this.updateCapabilitiesFromIpp(attrs);
}
// JetDirect and LPD don't need connection verification
@@ -154,7 +153,8 @@ export class PrintFeature extends Feature {
throw new Error('Print feature not connected');
}
return this.ippClient.getJobs();
const jobs = await this.ippClient.getJobs();
return jobs.map(job => this.mapIppJobToInternal(job));
}
/**
@@ -165,7 +165,8 @@ export class PrintFeature extends Feature {
throw new Error('Print feature not connected');
}
return this.ippClient.getJobInfo(jobId);
const job = await this.ippClient.getJobAttributes(jobId);
return this.mapIppJobToInternal(job);
}
/**
@@ -191,9 +192,17 @@ export class PrintFeature extends Feature {
this.emit('print:started', options);
// IppProtocol.print() accepts IPrintOptions and returns IPrintJob
const job = await this.ippClient.print(data, options);
// Use smartPrint for auto format detection and conversion
const ippJob = await this.ippClient.smartPrint(data, {
jobName: options?.jobName,
copies: options?.copies,
media: options?.mediaSize,
sides: options?.sides,
printQuality: options?.quality,
colorMode: options?.colorMode,
});
const job = this.mapIppJobToInternal(ippJob);
this.emit('print:submitted', job);
return job;
}
@@ -202,58 +211,57 @@ export class PrintFeature extends Feature {
// Helper Methods
// ============================================================================
private updateCapabilitiesFromIpp(caps: IPrinterCapabilities): void {
private updateCapabilitiesFromIpp(caps: IIppPrinterCapabilities): void {
this.supportsColor = caps.colorSupported;
this.supportsDuplex = caps.duplexSupported;
this.maxCopies = caps.maxCopies;
// Derive duplexSupported from sidesSupported
this.supportsDuplex = caps.sidesSupported?.some(s =>
s.includes('two-sided')
) ?? false;
// Get max copies from range
this.maxCopies = caps.copiesSupported?.upper ?? 99;
if (caps.mediaSizes && caps.mediaSizes.length > 0) {
this.supportedMediaSizes = caps.mediaSizes;
if (caps.mediaSizeSupported && caps.mediaSizeSupported.length > 0) {
this.supportedMediaSizes = caps.mediaSizeSupported;
}
if (caps.mediaTypes && caps.mediaTypes.length > 0) {
this.supportedMediaTypes = caps.mediaTypes;
if (caps.mediaTypeSupported && caps.mediaTypeSupported.length > 0) {
this.supportedMediaTypes = caps.mediaTypeSupported;
}
if (caps.sidesSupported && caps.sidesSupported.length > 0) {
this.supportedSides = caps.sidesSupported.filter((s): s is TPrintSides =>
['one-sided', 'two-sided-long-edge', 'two-sided-short-edge'].includes(s)
);
}
if (caps.qualitySupported && caps.qualitySupported.length > 0) {
this.supportedQualities = caps.qualitySupported.filter((q): q is TPrintQuality =>
['draft', 'normal', 'high'].includes(q)
);
// Map IPP quality values (3=draft, 4=normal, 5=high) to strings
if (caps.printQualitySupported && caps.printQualitySupported.length > 0) {
const qualityMap: Record<number, TPrintQuality> = { 3: 'draft', 4: 'normal', 5: 'high' };
this.supportedQualities = caps.printQualitySupported
.map(q => qualityMap[q])
.filter((q): q is TPrintQuality => q !== undefined);
}
}
private qualityToIpp(quality: TPrintQuality): number {
switch (quality) {
case 'draft': return 3;
case 'normal': return 4;
case 'high': return 5;
default: return 4;
}
}
private mapIppJob(job: Record<string, unknown>): IPrintJob {
const stateMap: Record<number, IPrintJob['state']> = {
3: 'pending',
4: 'pending',
5: 'processing',
6: 'processing',
7: 'canceled',
8: 'aborted',
9: 'completed',
/**
* Map IIppJob to IPrintJob, normalizing extended states
*/
private mapIppJobToInternal(job: IIppJob): IPrintJob {
// Map extended IPP states to simpler internal states
const stateMap: Record<IIppJob['state'], IPrintJob['state']> = {
'pending': 'pending',
'pending-held': 'pending',
'processing': 'processing',
'processing-stopped': 'processing',
'canceled': 'canceled',
'aborted': 'aborted',
'completed': 'completed',
};
return {
id: job['job-id'] as number,
name: job['job-name'] as string ?? 'Unknown',
state: stateMap[(job['job-state'] as number) ?? 3] ?? 'pending',
stateReason: (job['job-state-reasons'] as string[])?.[0],
createdAt: new Date((job['time-at-creation'] as number) * 1000),
completedAt: job['time-at-completed']
? new Date((job['time-at-completed'] as number) * 1000)
: undefined,
id: job.id,
name: job.name,
state: stateMap[job.state],
stateReason: job.stateReasons?.[0],
createdAt: job.createdAt,
completedAt: job.completedAt,
};
}

View File

@@ -10,7 +10,12 @@ export { EsclProtocol } from './protocol.escl.js';
export { SaneProtocol } from './protocol.sane.js';
// IPP printer protocol
export { IppProtocol } from './protocol.ipp.js';
export {
IppProtocol,
type IIppPrinterCapabilities,
type IIppJob,
type IIppPrintOptions,
} from './protocol.ipp.js';
// SNMP query protocol
export {

File diff suppressed because it is too large Load Diff