Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e424085000 | |||
| 82a99cdfb8 |
11
changelog.md
11
changelog.md
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-01-09 - 2.3.0 - feat(devicemanager)
|
||||
add selector-based device APIs, selectFeature helper, convenience discovery methods, and ESCL scan completion fallback
|
||||
|
||||
- Introduce IDeviceSelector and add selector support to getDevices(selector) to filter devices by id,address,name,model,manufacturer and feature capabilities.
|
||||
- Add selectDevice(selector) which returns exactly one device (throws if none) and logs a warning if multiple matches are returned without a unique identifier.
|
||||
- Deprecate getDevice(id) and getDeviceByAddress(address) in favor of selector-based retrieval methods.
|
||||
- Add private matchesSelector(...) implementing exact identity checks (id,address), case-insensitive partial attribute matching (name, model, manufacturer), and feature availability checks (hasFeature, hasFeatures, hasAnyFeature).
|
||||
- Add selectFeature(type) on devices to provide fail-fast access to a required feature (throws if missing).
|
||||
- Add discoverScanners(subnet, options) and discoverPrinters(subnet, options) convenience methods that run targeted network scans and return discovered scanners or printers respectively.
|
||||
- Improve ESCL protocol waitForScanComplete to attempt a direct download first (which triggers/blocks on many scanners) and fall back to polling if direct download fails or returns empty data.
|
||||
|
||||
## 2026-01-09 - 2.2.0 - feat(smarthome)
|
||||
add smart home features and Home Assistant integration (WebSocket protocol, discovery, factories, interfaces)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ecobridge.xyz/devicemanager",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0",
|
||||
"private": false,
|
||||
"description": "a device manager for talking to devices on network and over usb",
|
||||
"main": "dist_ts/index.js",
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@ecobridge.xyz/devicemanager',
|
||||
version: '2.2.0',
|
||||
version: '2.3.0',
|
||||
description: 'a device manager for talking to devices on network and over usb'
|
||||
}
|
||||
|
||||
@@ -246,12 +246,35 @@ export class UniversalDevice extends plugins.events.EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a feature by type
|
||||
* Get a feature by type (returns undefined if not available)
|
||||
*/
|
||||
public getFeature<T extends Feature>(type: TFeatureType): T | undefined {
|
||||
return this._features.get(type) as T | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a feature by type (throws if not available).
|
||||
* Use this when you expect the device to have this feature and want fail-fast behavior.
|
||||
*
|
||||
* @param type The feature type to select
|
||||
* @returns The feature instance
|
||||
* @throws Error if the device does not have this feature
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const scanFeature = device.selectFeature<ScanFeature>('scan');
|
||||
* await scanFeature.connect();
|
||||
* const result = await scanFeature.scan({ source: 'flatbed' });
|
||||
* ```
|
||||
*/
|
||||
public selectFeature<T extends Feature>(type: TFeatureType): T {
|
||||
const feature = this._features.get(type) as T | undefined;
|
||||
if (!feature) {
|
||||
throw new Error(`Device '${this.name}' does not have feature '${type}'`);
|
||||
}
|
||||
return feature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all features
|
||||
*/
|
||||
|
||||
@@ -38,6 +38,7 @@ import type {
|
||||
INetworkScanOptions,
|
||||
INetworkScanResult,
|
||||
TFeatureType,
|
||||
IDeviceSelector,
|
||||
} from './interfaces/index.js';
|
||||
|
||||
/**
|
||||
@@ -567,14 +568,48 @@ export class DeviceManager extends plugins.events.EventEmitter {
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get all devices
|
||||
* Get devices matching the selector criteria.
|
||||
* Returns all devices if no selector provided, or devices matching all criteria.
|
||||
* @param selector Optional criteria to filter devices
|
||||
* @returns Array of matching devices (empty if no matches)
|
||||
*/
|
||||
public getDevices(): UniversalDevice[] {
|
||||
return Array.from(this.devicesByIp.values());
|
||||
public getDevices(selector?: IDeviceSelector): UniversalDevice[] {
|
||||
const devices = Array.from(this.devicesByIp.values());
|
||||
|
||||
if (!selector) {
|
||||
return devices;
|
||||
}
|
||||
|
||||
return devices.filter((device) => this.matchesSelector(device, selector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Select exactly ONE device matching the criteria.
|
||||
* Use this when you expect a specific device and want fail-fast behavior.
|
||||
* @param selector Criteria to match the device
|
||||
* @returns The matching device
|
||||
* @throws Error if no device matches the selector
|
||||
*/
|
||||
public selectDevice(selector: IDeviceSelector): UniversalDevice {
|
||||
const matches = this.getDevices(selector);
|
||||
|
||||
if (matches.length === 0) {
|
||||
throw new Error(`No device found matching: ${JSON.stringify(selector)}`);
|
||||
}
|
||||
|
||||
if (matches.length > 1 && !selector.address && !selector.id) {
|
||||
// Multiple matches without unique identifier - log warning
|
||||
console.warn(
|
||||
`Multiple devices (${matches.length}) match selector, using first: ${matches[0].name}`
|
||||
);
|
||||
}
|
||||
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device by ID
|
||||
* @deprecated Use getDevices({ id }) or selectDevice({ id }) instead
|
||||
*/
|
||||
public getDevice(id: string): UniversalDevice | undefined {
|
||||
for (const device of this.devicesByIp.values()) {
|
||||
@@ -587,11 +622,52 @@ export class DeviceManager extends plugins.events.EventEmitter {
|
||||
|
||||
/**
|
||||
* Get device by address
|
||||
* @deprecated Use getDevices({ address }) or selectDevice({ address }) instead
|
||||
*/
|
||||
public getDeviceByAddress(address: string): UniversalDevice | undefined {
|
||||
return this.devicesByIp.get(address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a device matches the selector criteria
|
||||
*/
|
||||
private matchesSelector(device: UniversalDevice, selector: IDeviceSelector): boolean {
|
||||
// Identity checks (exact match)
|
||||
if (selector.id && device.id !== selector.id) {
|
||||
return false;
|
||||
}
|
||||
if (selector.address && device.address !== selector.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Attribute checks (partial match, case-insensitive)
|
||||
if (selector.name && !device.name.toLowerCase().includes(selector.name.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
if (selector.model && !device.model?.toLowerCase().includes(selector.model.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
selector.manufacturer &&
|
||||
!device.manufacturer?.toLowerCase().includes(selector.manufacturer.toLowerCase())
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Capability checks
|
||||
if (selector.hasFeature && !device.hasFeature(selector.hasFeature)) {
|
||||
return false;
|
||||
}
|
||||
if (selector.hasFeatures && !device.hasFeatures(selector.hasFeatures)) {
|
||||
return false;
|
||||
}
|
||||
if (selector.hasAnyFeature && !device.hasAnyFeature(selector.hasAnyFeature)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Device Access - By Feature Type
|
||||
// ============================================================================
|
||||
@@ -982,6 +1058,68 @@ export class DeviceManager extends plugins.events.EventEmitter {
|
||||
return foundDevices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover scanners in a subnet and add them to the manager.
|
||||
* Convenience method that focuses discovery on scanner protocols only (eSCL, SANE).
|
||||
*
|
||||
* @param subnet CIDR notation subnet (e.g., '192.168.1.0/24')
|
||||
* @param options Optional discovery settings
|
||||
* @returns Array of discovered scanner devices
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const scanners = await manager.discoverScanners('192.168.190.0/24');
|
||||
* for (const scanner of scanners) {
|
||||
* console.log(`Found: ${scanner.name} at ${scanner.address}`);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public async discoverScanners(
|
||||
subnet: string,
|
||||
options?: { timeout?: number; concurrency?: number }
|
||||
): Promise<UniversalDevice[]> {
|
||||
await this.scanNetwork({
|
||||
ipRange: subnet,
|
||||
probeEscl: true,
|
||||
probeSane: true,
|
||||
probeIpp: false,
|
||||
probeAirplay: false,
|
||||
probeSonos: false,
|
||||
probeChromecast: false,
|
||||
concurrency: options?.concurrency ?? 50,
|
||||
timeout: options?.timeout ?? 3000,
|
||||
});
|
||||
|
||||
return this.getScanners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover printers in a subnet and add them to the manager.
|
||||
* Convenience method that focuses discovery on printer protocols only (IPP).
|
||||
*
|
||||
* @param subnet CIDR notation subnet (e.g., '192.168.1.0/24')
|
||||
* @param options Optional discovery settings
|
||||
* @returns Array of discovered printer devices
|
||||
*/
|
||||
public async discoverPrinters(
|
||||
subnet: string,
|
||||
options?: { timeout?: number; concurrency?: number }
|
||||
): Promise<UniversalDevice[]> {
|
||||
await this.scanNetwork({
|
||||
ipRange: subnet,
|
||||
probeEscl: false,
|
||||
probeSane: false,
|
||||
probeIpp: true,
|
||||
probeAirplay: false,
|
||||
probeSonos: false,
|
||||
probeChromecast: false,
|
||||
concurrency: options?.concurrency ?? 50,
|
||||
timeout: options?.timeout ?? 3000,
|
||||
});
|
||||
|
||||
return this.getPrinters();
|
||||
}
|
||||
|
||||
public async cancelNetworkScan(): Promise<void> {
|
||||
if (this._networkScanner) {
|
||||
await this._networkScanner.cancel();
|
||||
|
||||
@@ -371,6 +371,37 @@ export type TNetworkScannerEvents = {
|
||||
'cancelled': () => void;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Device Selector Interface
|
||||
// ============================================================================
|
||||
|
||||
import type { TFeatureType } from './feature.interfaces.js';
|
||||
|
||||
/**
|
||||
* Criteria for selecting devices from the manager.
|
||||
* Identity selectors (id, address) match exactly.
|
||||
* Attribute selectors (name, model, manufacturer) match partially (case-insensitive).
|
||||
* Capability selectors filter by feature availability.
|
||||
*/
|
||||
export interface IDeviceSelector {
|
||||
/** Exact match on device ID */
|
||||
id?: string;
|
||||
/** Exact match on IP address */
|
||||
address?: string;
|
||||
/** Partial match on device name (case-insensitive) */
|
||||
name?: string;
|
||||
/** Partial match on device model (case-insensitive) */
|
||||
model?: string;
|
||||
/** Partial match on manufacturer name (case-insensitive) */
|
||||
manufacturer?: string;
|
||||
/** Device must have this feature */
|
||||
hasFeature?: TFeatureType;
|
||||
/** Device must have ALL of these features */
|
||||
hasFeatures?: TFeatureType[];
|
||||
/** Device must have ANY of these features */
|
||||
hasAnyFeature?: TFeatureType[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Feature Types (Universal Device Architecture)
|
||||
// ============================================================================
|
||||
|
||||
@@ -142,13 +142,28 @@ export class EsclProtocol {
|
||||
|
||||
/**
|
||||
* Wait for scan job to complete and download the result
|
||||
*
|
||||
* Note: Many scanners (including Brother) don't start scanning until
|
||||
* NextDocument is requested. The request triggers the scan and blocks
|
||||
* until complete. We try direct download first, then fall back to polling.
|
||||
*/
|
||||
public async waitForScanComplete(
|
||||
jobUri: string,
|
||||
options: IScanOptions,
|
||||
pollInterval: number = 500
|
||||
): Promise<IScanResult> {
|
||||
// Poll until job is complete
|
||||
// Try direct download first - this triggers the scan on many scanners
|
||||
// (including Brother) and blocks until the scan completes
|
||||
try {
|
||||
const result = await this.downloadScan(jobUri, options);
|
||||
if (result.data.length > 0) {
|
||||
return result;
|
||||
}
|
||||
} catch (err) {
|
||||
// Direct download failed, fall back to polling
|
||||
}
|
||||
|
||||
// Fall back to polling for scanners that need it
|
||||
let attempts = 0;
|
||||
const maxAttempts = 120; // 60 seconds max
|
||||
|
||||
|
||||
Reference in New Issue
Block a user