2026-01-09 07:14:39 +00:00
|
|
|
import * as plugins from './plugins.js';
|
|
|
|
|
import { MdnsDiscovery, SERVICE_TYPES } from './discovery/discovery.classes.mdns.js';
|
|
|
|
|
import { NetworkScanner } from './discovery/discovery.classes.networkscanner.js';
|
2026-01-09 09:03:42 +00:00
|
|
|
import { SsdpDiscovery, SSDP_SERVICE_TYPES, type ISsdpDevice } from './discovery/discovery.classes.ssdp.js';
|
2026-01-09 07:14:39 +00:00
|
|
|
import { Scanner } from './scanner/scanner.classes.scanner.js';
|
|
|
|
|
import { Printer } from './printer/printer.classes.printer.js';
|
2026-01-09 09:03:42 +00:00
|
|
|
import { SnmpDevice, type ISnmpDeviceInfo } from './snmp/snmp.classes.snmpdevice.js';
|
|
|
|
|
import { UpsDevice, type IUpsDeviceInfo, type TUpsProtocol } from './ups/ups.classes.upsdevice.js';
|
|
|
|
|
import { DlnaRenderer, type IDlnaRendererInfo } from './dlna/dlna.classes.renderer.js';
|
|
|
|
|
import { DlnaServer, type IDlnaServerInfo } from './dlna/dlna.classes.server.js';
|
|
|
|
|
import { Speaker, type ISpeakerInfo } from './speaker/speaker.classes.speaker.js';
|
|
|
|
|
import { SonosSpeaker, type ISonosSpeakerInfo } from './speaker/speaker.classes.sonos.js';
|
|
|
|
|
import { AirPlaySpeaker, type IAirPlaySpeakerInfo } from './speaker/speaker.classes.airplay.js';
|
|
|
|
|
import { ChromecastSpeaker, type IChromecastSpeakerInfo } from './speaker/speaker.classes.chromecast.js';
|
2026-01-09 07:14:39 +00:00
|
|
|
import type {
|
|
|
|
|
IDeviceManagerOptions,
|
|
|
|
|
IDiscoveredDevice,
|
|
|
|
|
IRetryOptions,
|
|
|
|
|
TDeviceManagerEvents,
|
|
|
|
|
INetworkScanOptions,
|
|
|
|
|
INetworkScanResult,
|
2026-01-09 09:03:42 +00:00
|
|
|
TDeviceType,
|
|
|
|
|
TFeatureType,
|
2026-01-09 07:14:39 +00:00
|
|
|
} from './interfaces/index.js';
|
|
|
|
|
|
2026-01-09 09:03:42 +00:00
|
|
|
// Universal Device & Features
|
|
|
|
|
import { UniversalDevice } from './device/device.classes.device.js';
|
|
|
|
|
import type { Feature } from './features/index.js';
|
|
|
|
|
|
2026-01-09 07:14:39 +00:00
|
|
|
/**
|
|
|
|
|
* Default device manager options
|
|
|
|
|
*/
|
|
|
|
|
const DEFAULT_OPTIONS: Required<IDeviceManagerOptions> = {
|
|
|
|
|
autoDiscovery: true,
|
|
|
|
|
discoveryTimeout: 10000,
|
|
|
|
|
enableRetry: true,
|
|
|
|
|
maxRetries: 5,
|
|
|
|
|
retryBaseDelay: 1000,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Main Device Manager class for discovering and managing network devices
|
|
|
|
|
*/
|
|
|
|
|
export class DeviceManager extends plugins.events.EventEmitter {
|
2026-01-09 09:03:42 +00:00
|
|
|
private mdnsDiscovery: MdnsDiscovery;
|
|
|
|
|
private ssdpDiscovery: SsdpDiscovery;
|
2026-01-09 07:14:39 +00:00
|
|
|
private _networkScanner: NetworkScanner | null = null;
|
2026-01-09 09:03:42 +00:00
|
|
|
|
|
|
|
|
// Device collections
|
2026-01-09 07:14:39 +00:00
|
|
|
private scanners: Map<string, Scanner> = new Map();
|
|
|
|
|
private printers: Map<string, Printer> = new Map();
|
2026-01-09 09:03:42 +00:00
|
|
|
private snmpDevices: Map<string, SnmpDevice> = new Map();
|
|
|
|
|
private upsDevices: Map<string, UpsDevice> = new Map();
|
|
|
|
|
private dlnaRenderers: Map<string, DlnaRenderer> = new Map();
|
|
|
|
|
private dlnaServers: Map<string, DlnaServer> = new Map();
|
|
|
|
|
private sonosSpeakers: Map<string, SonosSpeaker> = new Map();
|
|
|
|
|
private airplaySpeakers: Map<string, AirPlaySpeaker> = new Map();
|
|
|
|
|
private chromecastSpeakers: Map<string, ChromecastSpeaker> = new Map();
|
|
|
|
|
|
|
|
|
|
// Universal devices (new architecture)
|
|
|
|
|
private universalDevices: Map<string, UniversalDevice> = new Map();
|
|
|
|
|
|
2026-01-09 07:14:39 +00:00
|
|
|
private options: Required<IDeviceManagerOptions>;
|
|
|
|
|
private retryOptions: IRetryOptions;
|
|
|
|
|
|
|
|
|
|
constructor(options?: IDeviceManagerOptions) {
|
|
|
|
|
super();
|
|
|
|
|
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
|
|
|
|
|
|
|
|
this.retryOptions = {
|
|
|
|
|
maxRetries: this.options.maxRetries,
|
|
|
|
|
baseDelay: this.options.retryBaseDelay,
|
|
|
|
|
maxDelay: 16000,
|
|
|
|
|
multiplier: 2,
|
|
|
|
|
jitter: true,
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-09 09:03:42 +00:00
|
|
|
this.mdnsDiscovery = new MdnsDiscovery({
|
2026-01-09 07:14:39 +00:00
|
|
|
timeout: this.options.discoveryTimeout,
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-09 09:03:42 +00:00
|
|
|
this.ssdpDiscovery = new SsdpDiscovery();
|
|
|
|
|
|
2026-01-09 07:14:39 +00:00
|
|
|
this.setupDiscoveryEvents();
|
2026-01-09 09:03:42 +00:00
|
|
|
this.setupSsdpDiscoveryEvents();
|
2026-01-09 07:14:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-09 09:03:42 +00:00
|
|
|
* Setup event forwarding from mDNS discovery service
|
2026-01-09 07:14:39 +00:00
|
|
|
*/
|
|
|
|
|
private setupDiscoveryEvents(): void {
|
2026-01-09 09:03:42 +00:00
|
|
|
this.mdnsDiscovery.on('device:found', (device: IDiscoveredDevice) => {
|
|
|
|
|
this.handleMdnsDeviceFound(device);
|
2026-01-09 07:14:39 +00:00
|
|
|
});
|
|
|
|
|
|
2026-01-09 09:03:42 +00:00
|
|
|
this.mdnsDiscovery.on('device:lost', (device: IDiscoveredDevice) => {
|
2026-01-09 07:14:39 +00:00
|
|
|
this.handleDeviceLost(device);
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-09 09:03:42 +00:00
|
|
|
this.mdnsDiscovery.on('started', () => {
|
|
|
|
|
this.emit('discovery:started');
|
2026-01-09 07:14:39 +00:00
|
|
|
});
|
|
|
|
|
|
2026-01-09 09:03:42 +00:00
|
|
|
this.mdnsDiscovery.on('stopped', () => {
|
|
|
|
|
this.emit('discovery:stopped');
|
2026-01-09 07:14:39 +00:00
|
|
|
});
|
2026-01-09 09:03:42 +00:00
|
|
|
}
|
2026-01-09 07:14:39 +00:00
|
|
|
|
2026-01-09 09:03:42 +00:00
|
|
|
/**
|
|
|
|
|
* Setup event forwarding from SSDP discovery service
|
|
|
|
|
*/
|
|
|
|
|
private setupSsdpDiscoveryEvents(): void {
|
|
|
|
|
this.ssdpDiscovery.on('device:found', (device: ISsdpDevice) => {
|
|
|
|
|
this.handleSsdpDeviceFound(device);
|
2026-01-09 07:14:39 +00:00
|
|
|
});
|
|
|
|
|
|
2026-01-09 09:03:42 +00:00
|
|
|
this.ssdpDiscovery.on('started', () => {
|
|
|
|
|
this.emit('ssdp:started');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.ssdpDiscovery.on('stopped', () => {
|
|
|
|
|
this.emit('ssdp:stopped');
|
2026-01-09 07:14:39 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-09 09:03:42 +00:00
|
|
|
* Handle newly discovered mDNS device
|
2026-01-09 07:14:39 +00:00
|
|
|
*/
|
2026-01-09 09:03:42 +00:00
|
|
|
private handleMdnsDeviceFound(device: IDiscoveredDevice): void {
|
2026-01-09 07:14:39 +00:00
|
|
|
if (device.type === 'scanner') {
|
|
|
|
|
// Create Scanner instance
|
|
|
|
|
const scanner = Scanner.fromDiscovery(
|
|
|
|
|
{
|
|
|
|
|
id: device.id,
|
|
|
|
|
name: device.name,
|
|
|
|
|
address: device.address,
|
|
|
|
|
port: device.port,
|
|
|
|
|
protocol: device.protocol as 'sane' | 'escl',
|
|
|
|
|
txtRecords: device.txtRecords,
|
|
|
|
|
},
|
|
|
|
|
this.options.enableRetry ? this.retryOptions : undefined
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.scanners.set(device.id, scanner);
|
|
|
|
|
this.emit('scanner:found', scanner.getScannerInfo());
|
|
|
|
|
} else if (device.type === 'printer') {
|
|
|
|
|
// Create Printer instance
|
|
|
|
|
const printer = Printer.fromDiscovery(
|
|
|
|
|
{
|
|
|
|
|
id: device.id,
|
|
|
|
|
name: device.name,
|
|
|
|
|
address: device.address,
|
|
|
|
|
port: device.port,
|
|
|
|
|
txtRecords: device.txtRecords,
|
|
|
|
|
},
|
|
|
|
|
this.options.enableRetry ? this.retryOptions : undefined
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.printers.set(device.id, printer);
|
|
|
|
|
this.emit('printer:found', printer.getPrinterInfo());
|
2026-01-09 09:03:42 +00:00
|
|
|
} else if (device.type === 'speaker') {
|
|
|
|
|
// Create appropriate speaker instance based on protocol
|
|
|
|
|
this.handleMdnsSpeakerFound(device);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle mDNS speaker discovery
|
|
|
|
|
*/
|
|
|
|
|
private handleMdnsSpeakerFound(device: IDiscoveredDevice): void {
|
|
|
|
|
const protocol = device.protocol;
|
|
|
|
|
const txt = device.txtRecords || {};
|
|
|
|
|
|
|
|
|
|
if (protocol === 'sonos') {
|
|
|
|
|
// Sonos speaker
|
|
|
|
|
const speaker = SonosSpeaker.fromDiscovery(
|
|
|
|
|
{
|
|
|
|
|
id: device.id,
|
|
|
|
|
name: device.name,
|
|
|
|
|
address: device.address,
|
|
|
|
|
port: device.port || 1400,
|
|
|
|
|
roomName: txt['room'] || device.name,
|
|
|
|
|
modelName: txt['model'] || txt['md'],
|
|
|
|
|
},
|
|
|
|
|
this.options.enableRetry ? this.retryOptions : undefined
|
|
|
|
|
);
|
|
|
|
|
this.sonosSpeakers.set(device.id, speaker);
|
|
|
|
|
this.emit('speaker:found', speaker.getSpeakerInfo());
|
|
|
|
|
} else if (protocol === 'airplay') {
|
|
|
|
|
// AirPlay speaker (HomePod, Apple TV, etc.)
|
|
|
|
|
const features = txt['features'] ? parseInt(txt['features'], 16) : 0;
|
|
|
|
|
const speaker = AirPlaySpeaker.fromDiscovery(
|
|
|
|
|
{
|
|
|
|
|
id: device.id,
|
|
|
|
|
name: device.name,
|
|
|
|
|
address: device.address,
|
|
|
|
|
port: device.port || 7000,
|
|
|
|
|
roomName: txt['room'] || device.name,
|
|
|
|
|
modelName: txt['model'] || txt['am'],
|
|
|
|
|
features,
|
|
|
|
|
deviceId: txt['deviceid'] || txt['pk'],
|
|
|
|
|
},
|
|
|
|
|
this.options.enableRetry ? this.retryOptions : undefined
|
|
|
|
|
);
|
|
|
|
|
this.airplaySpeakers.set(device.id, speaker);
|
|
|
|
|
this.emit('speaker:found', speaker.getSpeakerInfo());
|
|
|
|
|
} else if (protocol === 'chromecast') {
|
|
|
|
|
// Chromecast / Google Cast
|
|
|
|
|
const speaker = ChromecastSpeaker.fromDiscovery(
|
|
|
|
|
{
|
|
|
|
|
id: device.id,
|
|
|
|
|
name: txt['fn'] || device.name,
|
|
|
|
|
address: device.address,
|
|
|
|
|
port: device.port || 8009,
|
|
|
|
|
friendlyName: txt['fn'] || device.name,
|
|
|
|
|
modelName: txt['md'],
|
|
|
|
|
},
|
|
|
|
|
this.options.enableRetry ? this.retryOptions : undefined
|
|
|
|
|
);
|
|
|
|
|
this.chromecastSpeakers.set(device.id, speaker);
|
|
|
|
|
this.emit('speaker:found', speaker.getSpeakerInfo());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle newly discovered SSDP/UPnP device
|
|
|
|
|
*/
|
|
|
|
|
private handleSsdpDeviceFound(device: ISsdpDevice): void {
|
|
|
|
|
if (!device.description) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const serviceType = device.serviceType;
|
|
|
|
|
const deviceType = device.description.deviceType;
|
|
|
|
|
|
|
|
|
|
// Check for DLNA Media Renderer
|
|
|
|
|
if (serviceType.includes('MediaRenderer') || deviceType.includes('MediaRenderer')) {
|
|
|
|
|
const renderer = DlnaRenderer.fromSsdpDevice(device, this.retryOptions);
|
|
|
|
|
if (renderer) {
|
|
|
|
|
this.dlnaRenderers.set(renderer.id, renderer);
|
|
|
|
|
this.emit('dlna:renderer:found', renderer.getDeviceInfo());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for DLNA Media Server
|
|
|
|
|
if (serviceType.includes('MediaServer') || deviceType.includes('MediaServer')) {
|
|
|
|
|
const server = DlnaServer.fromSsdpDevice(device, this.retryOptions);
|
|
|
|
|
if (server) {
|
|
|
|
|
this.dlnaServers.set(server.id, server);
|
|
|
|
|
this.emit('dlna:server:found', server.getDeviceInfo());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for Sonos ZonePlayer
|
|
|
|
|
if (serviceType.includes('ZonePlayer') || deviceType.includes('ZonePlayer')) {
|
|
|
|
|
const speaker = SonosSpeaker.fromDiscovery(
|
|
|
|
|
{
|
|
|
|
|
id: `sonos:${device.usn}`,
|
|
|
|
|
name: device.description.friendlyName,
|
|
|
|
|
address: device.address,
|
|
|
|
|
port: 1400,
|
|
|
|
|
roomName: device.description.friendlyName,
|
|
|
|
|
modelName: device.description.modelName,
|
|
|
|
|
},
|
|
|
|
|
this.retryOptions
|
|
|
|
|
);
|
|
|
|
|
this.sonosSpeakers.set(speaker.id, speaker);
|
|
|
|
|
this.emit('speaker:found', speaker.getSpeakerInfo());
|
2026-01-09 07:14:39 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle lost device
|
|
|
|
|
*/
|
|
|
|
|
private handleDeviceLost(device: IDiscoveredDevice): void {
|
|
|
|
|
if (device.type === 'scanner') {
|
|
|
|
|
const scanner = this.scanners.get(device.id);
|
|
|
|
|
if (scanner) {
|
|
|
|
|
// Disconnect if connected
|
|
|
|
|
if (scanner.isConnected) {
|
|
|
|
|
scanner.disconnect().catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
this.scanners.delete(device.id);
|
|
|
|
|
this.emit('scanner:lost', device.id);
|
|
|
|
|
}
|
|
|
|
|
} else if (device.type === 'printer') {
|
|
|
|
|
const printer = this.printers.get(device.id);
|
|
|
|
|
if (printer) {
|
|
|
|
|
// Disconnect if connected
|
|
|
|
|
if (printer.isConnected) {
|
|
|
|
|
printer.disconnect().catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
this.printers.delete(device.id);
|
|
|
|
|
this.emit('printer:lost', device.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-09 09:03:42 +00:00
|
|
|
* Start device discovery (mDNS and SSDP)
|
2026-01-09 07:14:39 +00:00
|
|
|
*/
|
|
|
|
|
public async startDiscovery(): Promise<void> {
|
2026-01-09 09:03:42 +00:00
|
|
|
await Promise.all([
|
|
|
|
|
this.mdnsDiscovery.start(),
|
|
|
|
|
this.ssdpDiscovery.start(),
|
|
|
|
|
]);
|
2026-01-09 07:14:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Stop device discovery
|
|
|
|
|
*/
|
|
|
|
|
public async stopDiscovery(): Promise<void> {
|
2026-01-09 09:03:42 +00:00
|
|
|
await Promise.all([
|
|
|
|
|
this.mdnsDiscovery.stop(),
|
|
|
|
|
this.ssdpDiscovery.stop(),
|
|
|
|
|
]);
|
2026-01-09 07:14:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if discovery is running
|
|
|
|
|
*/
|
|
|
|
|
public get isDiscovering(): boolean {
|
2026-01-09 09:03:42 +00:00
|
|
|
return this.mdnsDiscovery.running || this.ssdpDiscovery.isRunning;
|
2026-01-09 07:14:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all discovered scanners
|
|
|
|
|
*/
|
|
|
|
|
public getScanners(): Scanner[] {
|
|
|
|
|
return Array.from(this.scanners.values());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all discovered printers
|
|
|
|
|
*/
|
|
|
|
|
public getPrinters(): Printer[] {
|
|
|
|
|
return Array.from(this.printers.values());
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 09:03:42 +00:00
|
|
|
/**
|
|
|
|
|
* Get all discovered SNMP devices
|
|
|
|
|
*/
|
|
|
|
|
public getSnmpDevices(): SnmpDevice[] {
|
|
|
|
|
return Array.from(this.snmpDevices.values());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all discovered UPS devices
|
|
|
|
|
*/
|
|
|
|
|
public getUpsDevices(): UpsDevice[] {
|
|
|
|
|
return Array.from(this.upsDevices.values());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all DLNA media renderers
|
|
|
|
|
*/
|
|
|
|
|
public getDlnaRenderers(): DlnaRenderer[] {
|
|
|
|
|
return Array.from(this.dlnaRenderers.values());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all DLNA media servers
|
|
|
|
|
*/
|
|
|
|
|
public getDlnaServers(): DlnaServer[] {
|
|
|
|
|
return Array.from(this.dlnaServers.values());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all speakers (all protocols)
|
|
|
|
|
*/
|
|
|
|
|
public getSpeakers(): Speaker[] {
|
|
|
|
|
return [
|
|
|
|
|
...this.getSonosSpeakers(),
|
|
|
|
|
...this.getAirPlaySpeakers(),
|
|
|
|
|
...this.getChromecastSpeakers(),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all Sonos speakers
|
|
|
|
|
*/
|
|
|
|
|
public getSonosSpeakers(): SonosSpeaker[] {
|
|
|
|
|
return Array.from(this.sonosSpeakers.values());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all AirPlay speakers
|
|
|
|
|
*/
|
|
|
|
|
public getAirPlaySpeakers(): AirPlaySpeaker[] {
|
|
|
|
|
return Array.from(this.airplaySpeakers.values());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all Chromecast speakers
|
|
|
|
|
*/
|
|
|
|
|
public getChromecastSpeakers(): ChromecastSpeaker[] {
|
|
|
|
|
return Array.from(this.chromecastSpeakers.values());
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 07:14:39 +00:00
|
|
|
/**
|
|
|
|
|
* Get scanner by ID
|
|
|
|
|
*/
|
|
|
|
|
public getScanner(id: string): Scanner | undefined {
|
|
|
|
|
return this.scanners.get(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get printer by ID
|
|
|
|
|
*/
|
|
|
|
|
public getPrinter(id: string): Printer | undefined {
|
|
|
|
|
return this.printers.get(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-09 09:03:42 +00:00
|
|
|
* Get SNMP device by ID
|
|
|
|
|
*/
|
|
|
|
|
public getSnmpDevice(id: string): SnmpDevice | undefined {
|
|
|
|
|
return this.snmpDevices.get(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get UPS device by ID
|
|
|
|
|
*/
|
|
|
|
|
public getUpsDevice(id: string): UpsDevice | undefined {
|
|
|
|
|
return this.upsDevices.get(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get DLNA renderer by ID
|
|
|
|
|
*/
|
|
|
|
|
public getDlnaRenderer(id: string): DlnaRenderer | undefined {
|
|
|
|
|
return this.dlnaRenderers.get(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get DLNA server by ID
|
|
|
|
|
*/
|
|
|
|
|
public getDlnaServer(id: string): DlnaServer | undefined {
|
|
|
|
|
return this.dlnaServers.get(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get speaker by ID (any protocol)
|
2026-01-09 07:14:39 +00:00
|
|
|
*/
|
2026-01-09 09:03:42 +00:00
|
|
|
public getSpeaker(id: string): Speaker | undefined {
|
|
|
|
|
return this.sonosSpeakers.get(id) ??
|
|
|
|
|
this.airplaySpeakers.get(id) ??
|
|
|
|
|
this.chromecastSpeakers.get(id);
|
2026-01-09 07:14:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-09 09:03:42 +00:00
|
|
|
* Get all devices (all types)
|
2026-01-09 07:14:39 +00:00
|
|
|
*/
|
2026-01-09 09:03:42 +00:00
|
|
|
public getAllDevices(): (Scanner | Printer | SnmpDevice | UpsDevice | DlnaRenderer | DlnaServer | Speaker)[] {
|
|
|
|
|
return [
|
|
|
|
|
...this.getScanners(),
|
|
|
|
|
...this.getPrinters(),
|
|
|
|
|
...this.getSnmpDevices(),
|
|
|
|
|
...this.getUpsDevices(),
|
|
|
|
|
...this.getDlnaRenderers(),
|
|
|
|
|
...this.getDlnaServers(),
|
|
|
|
|
...this.getSpeakers(),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get devices by type
|
|
|
|
|
*/
|
|
|
|
|
public getDevicesByType(type: TDeviceType): (Scanner | Printer | SnmpDevice | UpsDevice | DlnaRenderer | DlnaServer | Speaker)[] {
|
|
|
|
|
switch (type) {
|
|
|
|
|
case 'scanner':
|
|
|
|
|
return this.getScanners();
|
|
|
|
|
case 'printer':
|
|
|
|
|
return this.getPrinters();
|
|
|
|
|
case 'snmp':
|
|
|
|
|
return this.getSnmpDevices();
|
|
|
|
|
case 'ups':
|
|
|
|
|
return this.getUpsDevices();
|
|
|
|
|
case 'dlna-renderer':
|
|
|
|
|
return this.getDlnaRenderers();
|
|
|
|
|
case 'dlna-server':
|
|
|
|
|
return this.getDlnaServers();
|
|
|
|
|
case 'speaker':
|
|
|
|
|
return this.getSpeakers();
|
|
|
|
|
default:
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get device by ID (any type)
|
|
|
|
|
*/
|
|
|
|
|
public getDeviceById(id: string): Scanner | Printer | SnmpDevice | UpsDevice | DlnaRenderer | DlnaServer | Speaker | undefined {
|
|
|
|
|
return this.scanners.get(id) ??
|
|
|
|
|
this.printers.get(id) ??
|
|
|
|
|
this.snmpDevices.get(id) ??
|
|
|
|
|
this.upsDevices.get(id) ??
|
|
|
|
|
this.dlnaRenderers.get(id) ??
|
|
|
|
|
this.dlnaServers.get(id) ??
|
|
|
|
|
this.getSpeaker(id);
|
2026-01-09 07:14:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add a scanner manually (without discovery)
|
|
|
|
|
*/
|
|
|
|
|
public async addScanner(
|
|
|
|
|
address: string,
|
|
|
|
|
port: number,
|
|
|
|
|
protocol: 'escl' | 'sane' = 'escl',
|
|
|
|
|
name?: string
|
|
|
|
|
): Promise<Scanner> {
|
|
|
|
|
const id = `manual:${protocol}:${address}:${port}`;
|
|
|
|
|
|
|
|
|
|
// Check if already exists
|
|
|
|
|
if (this.scanners.has(id)) {
|
|
|
|
|
return this.scanners.get(id)!;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const scanner = Scanner.fromDiscovery(
|
|
|
|
|
{
|
|
|
|
|
id,
|
|
|
|
|
name: name ?? `Scanner at ${address}`,
|
|
|
|
|
address,
|
|
|
|
|
port,
|
|
|
|
|
protocol,
|
|
|
|
|
txtRecords: {},
|
|
|
|
|
},
|
|
|
|
|
this.options.enableRetry ? this.retryOptions : undefined
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Try to connect to validate
|
|
|
|
|
await scanner.connect();
|
|
|
|
|
|
|
|
|
|
this.scanners.set(id, scanner);
|
|
|
|
|
this.emit('scanner:found', scanner.getScannerInfo());
|
|
|
|
|
|
|
|
|
|
return scanner;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add a printer manually (without discovery)
|
|
|
|
|
*/
|
|
|
|
|
public async addPrinter(
|
|
|
|
|
address: string,
|
|
|
|
|
port: number = 631,
|
|
|
|
|
name?: string,
|
|
|
|
|
ippPath?: string
|
|
|
|
|
): Promise<Printer> {
|
|
|
|
|
const id = `manual:ipp:${address}:${port}`;
|
|
|
|
|
|
|
|
|
|
// Check if already exists
|
|
|
|
|
if (this.printers.has(id)) {
|
|
|
|
|
return this.printers.get(id)!;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const printer = Printer.fromDiscovery(
|
|
|
|
|
{
|
|
|
|
|
id,
|
|
|
|
|
name: name ?? `Printer at ${address}`,
|
|
|
|
|
address,
|
|
|
|
|
port,
|
|
|
|
|
txtRecords: ippPath ? { rp: ippPath } : {},
|
|
|
|
|
},
|
|
|
|
|
this.options.enableRetry ? this.retryOptions : undefined
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Try to connect to validate
|
|
|
|
|
await printer.connect();
|
|
|
|
|
|
|
|
|
|
this.printers.set(id, printer);
|
|
|
|
|
this.emit('printer:found', printer.getPrinterInfo());
|
|
|
|
|
|
|
|
|
|
return printer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the NetworkScanner instance for advanced control
|
|
|
|
|
*/
|
|
|
|
|
public get networkScanner(): NetworkScanner {
|
|
|
|
|
if (!this._networkScanner) {
|
|
|
|
|
this._networkScanner = new NetworkScanner();
|
|
|
|
|
this.setupNetworkScannerEvents();
|
|
|
|
|
}
|
|
|
|
|
return this._networkScanner;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Setup event forwarding from network scanner
|
|
|
|
|
*/
|
|
|
|
|
private setupNetworkScannerEvents(): void {
|
|
|
|
|
if (!this._networkScanner) return;
|
|
|
|
|
|
|
|
|
|
this._networkScanner.on('device:found', (result: INetworkScanResult) => {
|
|
|
|
|
this.emit('network:device:found', result);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this._networkScanner.on('progress', (progress) => {
|
|
|
|
|
this.emit('network:progress', progress);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this._networkScanner.on('complete', (results) => {
|
|
|
|
|
this.emit('network:complete', results);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this._networkScanner.on('error', (error) => {
|
|
|
|
|
this.emit('error', error);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Scan a network range for devices (IP-based, not mDNS)
|
|
|
|
|
* Found devices are automatically added to the device manager
|
|
|
|
|
*/
|
|
|
|
|
public async scanNetwork(
|
|
|
|
|
options: INetworkScanOptions
|
2026-01-09 09:03:42 +00:00
|
|
|
): Promise<{ scanners: Scanner[]; printers: Printer[]; speakers: Speaker[] }> {
|
2026-01-09 07:14:39 +00:00
|
|
|
const results = await this.networkScanner.scan(options);
|
|
|
|
|
const foundScanners: Scanner[] = [];
|
|
|
|
|
const foundPrinters: Printer[] = [];
|
2026-01-09 09:03:42 +00:00
|
|
|
const foundSpeakers: Speaker[] = [];
|
2026-01-09 07:14:39 +00:00
|
|
|
|
|
|
|
|
for (const result of results) {
|
|
|
|
|
for (const device of result.devices) {
|
|
|
|
|
try {
|
|
|
|
|
if (device.type === 'scanner') {
|
|
|
|
|
if (device.protocol === 'escl') {
|
|
|
|
|
const scanner = await this.addScanner(
|
|
|
|
|
result.address,
|
|
|
|
|
device.port,
|
|
|
|
|
'escl',
|
|
|
|
|
device.name
|
|
|
|
|
);
|
|
|
|
|
foundScanners.push(scanner);
|
|
|
|
|
} else if (device.protocol === 'sane') {
|
|
|
|
|
const scanner = await this.addScanner(
|
|
|
|
|
result.address,
|
|
|
|
|
device.port,
|
|
|
|
|
'sane',
|
|
|
|
|
device.name
|
|
|
|
|
);
|
|
|
|
|
foundScanners.push(scanner);
|
|
|
|
|
}
|
|
|
|
|
} else if (device.type === 'printer') {
|
|
|
|
|
if (device.protocol === 'ipp') {
|
|
|
|
|
const printer = await this.addPrinter(
|
|
|
|
|
result.address,
|
|
|
|
|
device.port,
|
|
|
|
|
device.name
|
|
|
|
|
);
|
|
|
|
|
foundPrinters.push(printer);
|
|
|
|
|
}
|
|
|
|
|
// JetDirect printers don't have a protocol handler yet
|
2026-01-09 09:03:42 +00:00
|
|
|
} else if (device.type === 'speaker') {
|
|
|
|
|
if (device.protocol === 'airplay') {
|
|
|
|
|
const speaker = await this.addAirPlaySpeaker(
|
|
|
|
|
result.address,
|
|
|
|
|
{ name: device.name, port: device.port }
|
|
|
|
|
);
|
|
|
|
|
foundSpeakers.push(speaker);
|
|
|
|
|
} else if (device.protocol === 'sonos') {
|
|
|
|
|
const speaker = await this.addSonosSpeaker(
|
|
|
|
|
result.address,
|
|
|
|
|
{ name: device.name, port: device.port }
|
|
|
|
|
);
|
|
|
|
|
foundSpeakers.push(speaker);
|
|
|
|
|
} else if (device.protocol === 'chromecast') {
|
|
|
|
|
const speaker = await this.addChromecastSpeaker(
|
|
|
|
|
result.address,
|
|
|
|
|
{ name: device.name, port: device.port }
|
|
|
|
|
);
|
|
|
|
|
foundSpeakers.push(speaker);
|
|
|
|
|
}
|
2026-01-09 07:14:39 +00:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Device could not be added (connection failed, etc.)
|
|
|
|
|
this.emit('error', error instanceof Error ? error : new Error(String(error)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 09:03:42 +00:00
|
|
|
return { scanners: foundScanners, printers: foundPrinters, speakers: foundSpeakers };
|
2026-01-09 07:14:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Cancel an ongoing network scan
|
|
|
|
|
*/
|
|
|
|
|
public async cancelNetworkScan(): Promise<void> {
|
|
|
|
|
if (this._networkScanner) {
|
|
|
|
|
await this._networkScanner.cancel();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a network scan is in progress
|
|
|
|
|
*/
|
|
|
|
|
public get isNetworkScanning(): boolean {
|
|
|
|
|
return this._networkScanner?.isScanning ?? false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove a device
|
|
|
|
|
*/
|
|
|
|
|
public async removeDevice(id: string): Promise<boolean> {
|
|
|
|
|
const scanner = this.scanners.get(id);
|
|
|
|
|
if (scanner) {
|
|
|
|
|
if (scanner.isConnected) {
|
|
|
|
|
await scanner.disconnect();
|
|
|
|
|
}
|
|
|
|
|
this.scanners.delete(id);
|
|
|
|
|
this.emit('scanner:lost', id);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const printer = this.printers.get(id);
|
|
|
|
|
if (printer) {
|
|
|
|
|
if (printer.isConnected) {
|
|
|
|
|
await printer.disconnect();
|
|
|
|
|
}
|
|
|
|
|
this.printers.delete(id);
|
|
|
|
|
this.emit('printer:lost', id);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Disconnect all devices
|
|
|
|
|
*/
|
|
|
|
|
public async disconnectAll(): Promise<void> {
|
|
|
|
|
const disconnectPromises: Promise<void>[] = [];
|
|
|
|
|
|
2026-01-09 09:03:42 +00:00
|
|
|
// Disconnect all device types
|
|
|
|
|
const allDevices = this.getAllDevices();
|
|
|
|
|
for (const device of allDevices) {
|
|
|
|
|
if (device.isConnected) {
|
|
|
|
|
disconnectPromises.push(device.disconnect().catch(() => {}));
|
2026-01-09 07:14:39 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await Promise.all(disconnectPromises);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Stop discovery and disconnect all devices
|
|
|
|
|
*/
|
|
|
|
|
public async shutdown(): Promise<void> {
|
|
|
|
|
await this.stopDiscovery();
|
|
|
|
|
await this.disconnectAll();
|
2026-01-09 09:03:42 +00:00
|
|
|
|
|
|
|
|
// Clear all device collections
|
2026-01-09 07:14:39 +00:00
|
|
|
this.scanners.clear();
|
|
|
|
|
this.printers.clear();
|
2026-01-09 09:03:42 +00:00
|
|
|
this.snmpDevices.clear();
|
|
|
|
|
this.upsDevices.clear();
|
|
|
|
|
this.dlnaRenderers.clear();
|
|
|
|
|
this.dlnaServers.clear();
|
|
|
|
|
this.sonosSpeakers.clear();
|
|
|
|
|
this.airplaySpeakers.clear();
|
|
|
|
|
this.chromecastSpeakers.clear();
|
|
|
|
|
this.universalDevices.clear();
|
2026-01-09 07:14:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Refresh status of all devices
|
|
|
|
|
*/
|
|
|
|
|
public async refreshAllStatus(): Promise<void> {
|
|
|
|
|
const refreshPromises: Promise<void>[] = [];
|
|
|
|
|
|
2026-01-09 09:03:42 +00:00
|
|
|
const allDevices = this.getAllDevices();
|
|
|
|
|
for (const device of allDevices) {
|
|
|
|
|
if (device.isConnected) {
|
2026-01-09 07:14:39 +00:00
|
|
|
refreshPromises.push(
|
2026-01-09 09:03:42 +00:00
|
|
|
device.refreshStatus().catch((error) => {
|
2026-01-09 07:14:39 +00:00
|
|
|
this.emit('error', error);
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 09:03:42 +00:00
|
|
|
await Promise.all(refreshPromises);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Manual Device Addition Methods
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add an SNMP device manually
|
|
|
|
|
*/
|
|
|
|
|
public async addSnmpDevice(
|
|
|
|
|
address: string,
|
|
|
|
|
port: number = 161,
|
|
|
|
|
options?: { name?: string; community?: string }
|
|
|
|
|
): Promise<SnmpDevice> {
|
|
|
|
|
const id = `manual:snmp:${address}:${port}`;
|
|
|
|
|
|
|
|
|
|
if (this.snmpDevices.has(id)) {
|
|
|
|
|
return this.snmpDevices.get(id)!;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const device = SnmpDevice.fromDiscovery(
|
|
|
|
|
{
|
|
|
|
|
id,
|
|
|
|
|
name: options?.name ?? `SNMP Device at ${address}`,
|
|
|
|
|
address,
|
|
|
|
|
port,
|
|
|
|
|
community: options?.community ?? 'public',
|
|
|
|
|
},
|
|
|
|
|
this.retryOptions
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await device.connect();
|
|
|
|
|
this.snmpDevices.set(id, device);
|
|
|
|
|
this.emit('snmp:found', device.getDeviceInfo());
|
|
|
|
|
|
|
|
|
|
return device;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add a UPS device manually
|
|
|
|
|
*/
|
|
|
|
|
public async addUpsDevice(
|
|
|
|
|
address: string,
|
|
|
|
|
protocol: TUpsProtocol,
|
|
|
|
|
options?: { name?: string; port?: number; upsName?: string; community?: string }
|
|
|
|
|
): Promise<UpsDevice> {
|
|
|
|
|
const port = options?.port ?? (protocol === 'nut' ? 3493 : 161);
|
|
|
|
|
const id = `manual:ups:${address}:${port}`;
|
|
|
|
|
|
|
|
|
|
if (this.upsDevices.has(id)) {
|
|
|
|
|
return this.upsDevices.get(id)!;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const device = UpsDevice.fromDiscovery(
|
|
|
|
|
{
|
|
|
|
|
id,
|
|
|
|
|
name: options?.name ?? `UPS at ${address}`,
|
|
|
|
|
address,
|
|
|
|
|
port,
|
|
|
|
|
protocol,
|
|
|
|
|
upsName: options?.upsName,
|
|
|
|
|
community: options?.community,
|
|
|
|
|
},
|
|
|
|
|
this.retryOptions
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await device.connect();
|
|
|
|
|
this.upsDevices.set(id, device);
|
|
|
|
|
this.emit('ups:found', device.getDeviceInfo());
|
|
|
|
|
|
|
|
|
|
return device;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add a Sonos speaker manually
|
|
|
|
|
*/
|
|
|
|
|
public async addSonosSpeaker(
|
|
|
|
|
address: string,
|
|
|
|
|
options?: { name?: string; port?: number }
|
|
|
|
|
): Promise<SonosSpeaker> {
|
|
|
|
|
const port = options?.port ?? 1400;
|
|
|
|
|
const id = `manual:sonos:${address}:${port}`;
|
|
|
|
|
|
|
|
|
|
if (this.sonosSpeakers.has(id)) {
|
|
|
|
|
return this.sonosSpeakers.get(id)!;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const speaker = SonosSpeaker.fromDiscovery(
|
|
|
|
|
{
|
|
|
|
|
id,
|
|
|
|
|
name: options?.name ?? `Sonos at ${address}`,
|
|
|
|
|
address,
|
|
|
|
|
port,
|
|
|
|
|
},
|
|
|
|
|
this.retryOptions
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await speaker.connect();
|
|
|
|
|
this.sonosSpeakers.set(id, speaker);
|
|
|
|
|
this.emit('speaker:found', speaker.getSpeakerInfo());
|
|
|
|
|
|
|
|
|
|
return speaker;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add an AirPlay speaker manually
|
|
|
|
|
*/
|
|
|
|
|
public async addAirPlaySpeaker(
|
|
|
|
|
address: string,
|
|
|
|
|
options?: { name?: string; port?: number }
|
|
|
|
|
): Promise<AirPlaySpeaker> {
|
|
|
|
|
const port = options?.port ?? 7000;
|
|
|
|
|
const id = `manual:airplay:${address}:${port}`;
|
|
|
|
|
|
|
|
|
|
if (this.airplaySpeakers.has(id)) {
|
|
|
|
|
return this.airplaySpeakers.get(id)!;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const speaker = AirPlaySpeaker.fromDiscovery(
|
|
|
|
|
{
|
|
|
|
|
id,
|
|
|
|
|
name: options?.name ?? `AirPlay at ${address}`,
|
|
|
|
|
address,
|
|
|
|
|
port,
|
|
|
|
|
},
|
|
|
|
|
this.retryOptions
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await speaker.connect();
|
|
|
|
|
this.airplaySpeakers.set(id, speaker);
|
|
|
|
|
this.emit('speaker:found', speaker.getSpeakerInfo());
|
|
|
|
|
|
|
|
|
|
return speaker;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add a Chromecast speaker manually
|
|
|
|
|
*/
|
|
|
|
|
public async addChromecastSpeaker(
|
|
|
|
|
address: string,
|
|
|
|
|
options?: { name?: string; port?: number }
|
|
|
|
|
): Promise<ChromecastSpeaker> {
|
|
|
|
|
const port = options?.port ?? 8009;
|
|
|
|
|
const id = `manual:chromecast:${address}:${port}`;
|
|
|
|
|
|
|
|
|
|
if (this.chromecastSpeakers.has(id)) {
|
|
|
|
|
return this.chromecastSpeakers.get(id)!;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const speaker = ChromecastSpeaker.fromDiscovery(
|
|
|
|
|
{
|
|
|
|
|
id,
|
|
|
|
|
name: options?.name ?? `Chromecast at ${address}`,
|
|
|
|
|
address,
|
|
|
|
|
port,
|
|
|
|
|
},
|
|
|
|
|
this.retryOptions
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await speaker.connect();
|
|
|
|
|
this.chromecastSpeakers.set(id, speaker);
|
|
|
|
|
this.emit('speaker:found', speaker.getSpeakerInfo());
|
|
|
|
|
|
|
|
|
|
return speaker;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Universal Device & Feature-Based API (New Architecture)
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all universal devices
|
|
|
|
|
*/
|
|
|
|
|
public getUniversalDevices(): UniversalDevice[] {
|
|
|
|
|
return Array.from(this.universalDevices.values());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a universal device by ID
|
|
|
|
|
*/
|
|
|
|
|
public getUniversalDevice(id: string): UniversalDevice | undefined {
|
|
|
|
|
return this.universalDevices.get(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a universal device by address
|
|
|
|
|
*/
|
|
|
|
|
public getUniversalDeviceByAddress(address: string): UniversalDevice | undefined {
|
|
|
|
|
for (const device of this.universalDevices.values()) {
|
|
|
|
|
if (device.address === address) {
|
|
|
|
|
return device;
|
2026-01-09 07:14:39 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-09 09:03:42 +00:00
|
|
|
return undefined;
|
|
|
|
|
}
|
2026-01-09 07:14:39 +00:00
|
|
|
|
2026-01-09 09:03:42 +00:00
|
|
|
/**
|
|
|
|
|
* Get all universal devices that have a specific feature
|
|
|
|
|
* @param featureType The feature type to search for
|
|
|
|
|
*/
|
|
|
|
|
public getDevicesWithFeature(featureType: TFeatureType): UniversalDevice[] {
|
|
|
|
|
return this.getUniversalDevices().filter(device => device.hasFeature(featureType));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all universal devices that have ALL specified features
|
|
|
|
|
* @param featureTypes Array of feature types - device must have ALL of them
|
|
|
|
|
*/
|
|
|
|
|
public getDevicesWithFeatures(featureTypes: TFeatureType[]): UniversalDevice[] {
|
|
|
|
|
return this.getUniversalDevices().filter(device => device.hasFeatures(featureTypes));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all universal devices that have ANY of the specified features
|
|
|
|
|
* @param featureTypes Array of feature types - device must have at least one
|
|
|
|
|
*/
|
|
|
|
|
public getDevicesWithAnyFeature(featureTypes: TFeatureType[]): UniversalDevice[] {
|
|
|
|
|
return this.getUniversalDevices().filter(device =>
|
|
|
|
|
featureTypes.some(type => device.hasFeature(type))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add a universal device
|
|
|
|
|
*/
|
|
|
|
|
public addUniversalDevice(device: UniversalDevice): void {
|
|
|
|
|
const existingDevice = this.universalDevices.get(device.id);
|
|
|
|
|
if (existingDevice) {
|
|
|
|
|
// Merge features into existing device
|
|
|
|
|
for (const feature of device.getFeatures()) {
|
|
|
|
|
if (!existingDevice.hasFeature(feature.type)) {
|
|
|
|
|
existingDevice.addFeature(feature);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.universalDevices.set(device.id, device);
|
|
|
|
|
this.emit('universal:device:found', device);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add a feature to an existing universal device
|
|
|
|
|
*/
|
|
|
|
|
public addFeatureToDevice(deviceId: string, feature: Feature): boolean {
|
|
|
|
|
const device = this.universalDevices.get(deviceId);
|
|
|
|
|
if (device) {
|
|
|
|
|
device.addFeature(feature);
|
|
|
|
|
this.emit('feature:added', { device, feature });
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove a feature from a universal device
|
|
|
|
|
*/
|
|
|
|
|
public async removeFeatureFromDevice(deviceId: string, featureType: TFeatureType): Promise<boolean> {
|
|
|
|
|
const device = this.universalDevices.get(deviceId);
|
|
|
|
|
if (device) {
|
|
|
|
|
const removed = await device.removeFeature(featureType);
|
|
|
|
|
if (removed) {
|
|
|
|
|
this.emit('feature:removed', { device, featureType });
|
|
|
|
|
}
|
|
|
|
|
return removed;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove a universal device
|
|
|
|
|
*/
|
|
|
|
|
public async removeUniversalDevice(id: string): Promise<boolean> {
|
|
|
|
|
const device = this.universalDevices.get(id);
|
|
|
|
|
if (device) {
|
|
|
|
|
await device.disconnect();
|
|
|
|
|
this.universalDevices.delete(id);
|
|
|
|
|
this.emit('universal:device:lost', id);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get feature from a device by type
|
|
|
|
|
*/
|
|
|
|
|
public getFeatureFromDevice<T extends Feature>(deviceId: string, featureType: TFeatureType): T | undefined {
|
|
|
|
|
const device = this.universalDevices.get(deviceId);
|
|
|
|
|
return device?.getFeature<T>(featureType);
|
2026-01-09 07:14:39 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 09:03:42 +00:00
|
|
|
// Export all classes and types
|
|
|
|
|
export {
|
|
|
|
|
// Discovery
|
|
|
|
|
MdnsDiscovery,
|
|
|
|
|
NetworkScanner,
|
|
|
|
|
SsdpDiscovery,
|
|
|
|
|
SERVICE_TYPES,
|
|
|
|
|
SSDP_SERVICE_TYPES,
|
|
|
|
|
|
|
|
|
|
// Scanner & Printer
|
|
|
|
|
Scanner,
|
|
|
|
|
Printer,
|
|
|
|
|
|
|
|
|
|
// SNMP
|
|
|
|
|
SnmpDevice,
|
|
|
|
|
|
|
|
|
|
// UPS
|
|
|
|
|
UpsDevice,
|
|
|
|
|
|
|
|
|
|
// DLNA
|
|
|
|
|
DlnaRenderer,
|
|
|
|
|
DlnaServer,
|
|
|
|
|
|
|
|
|
|
// Speakers
|
|
|
|
|
Speaker,
|
|
|
|
|
SonosSpeaker,
|
|
|
|
|
AirPlaySpeaker,
|
|
|
|
|
ChromecastSpeaker,
|
|
|
|
|
|
|
|
|
|
// Universal Device (new architecture)
|
|
|
|
|
UniversalDevice,
|
|
|
|
|
};
|