feat(devicemanager): add selector-based device APIs, selectFeature helper, convenience discovery methods, and ESCL scan completion fallback

This commit is contained in:
2026-01-09 17:18:48 +00:00
parent d72ea96ec5
commit 82a99cdfb8
6 changed files with 224 additions and 6 deletions

View File

@@ -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();