feat(devicemanager): Introduce a UniversalDevice architecture with composable Feature system; add extensive new device/protocol support and discovery/refactors

This commit is contained in:
2026-01-09 09:03:42 +00:00
parent 05e1f94c79
commit 206b4b5ae0
33 changed files with 8254 additions and 87 deletions

View File

@@ -0,0 +1,216 @@
import * as plugins from '../plugins.js';
import { Device } from '../abstract/device.abstract.js';
import type { IDeviceInfo, IRetryOptions } from '../interfaces/index.js';
/**
* Speaker protocol types
*/
export type TSpeakerProtocol = 'sonos' | 'airplay' | 'chromecast' | 'dlna';
/**
* Playback state
*/
export type TPlaybackState = 'playing' | 'paused' | 'stopped' | 'transitioning' | 'unknown';
/**
* Track information
*/
export interface ITrackInfo {
title: string;
artist?: string;
album?: string;
duration: number; // seconds
position: number; // seconds
albumArtUri?: string;
uri?: string;
}
/**
* Speaker playback status
*/
export interface IPlaybackStatus {
state: TPlaybackState;
volume: number; // 0-100
muted: boolean;
track?: ITrackInfo;
}
/**
* Speaker device info
*/
export interface ISpeakerInfo extends IDeviceInfo {
type: 'speaker';
protocol: TSpeakerProtocol;
roomName?: string;
modelName?: string;
supportsGrouping?: boolean;
groupId?: string;
isGroupCoordinator?: boolean;
}
/**
* Abstract Speaker base class
* Common interface for all speaker types (Sonos, AirPlay, Chromecast)
*/
export abstract class Speaker extends Device {
protected _protocol: TSpeakerProtocol;
protected _roomName?: string;
protected _modelName?: string;
protected _volume: number = 0;
protected _muted: boolean = false;
protected _playbackState: TPlaybackState = 'unknown';
constructor(
info: IDeviceInfo,
protocol: TSpeakerProtocol,
options?: {
roomName?: string;
modelName?: string;
},
retryOptions?: IRetryOptions
) {
super(info, retryOptions);
this._protocol = protocol;
this._roomName = options?.roomName;
this._modelName = options?.modelName;
}
// Getters
public get protocol(): TSpeakerProtocol {
return this._protocol;
}
public get roomName(): string | undefined {
return this._roomName;
}
public get speakerModelName(): string | undefined {
return this._modelName;
}
public get volume(): number {
return this._volume;
}
public get muted(): boolean {
return this._muted;
}
public get playbackState(): TPlaybackState {
return this._playbackState;
}
// ============================================================================
// Abstract Methods - Must be implemented by subclasses
// ============================================================================
/**
* Play media from URI
*/
public abstract play(uri?: string): Promise<void>;
/**
* Pause playback
*/
public abstract pause(): Promise<void>;
/**
* Stop playback
*/
public abstract stop(): Promise<void>;
/**
* Next track
*/
public abstract next(): Promise<void>;
/**
* Previous track
*/
public abstract previous(): Promise<void>;
/**
* Seek to position
*/
public abstract seek(seconds: number): Promise<void>;
/**
* Get volume level (0-100)
*/
public abstract getVolume(): Promise<number>;
/**
* Set volume level (0-100)
*/
public abstract setVolume(level: number): Promise<void>;
/**
* Get mute state
*/
public abstract getMute(): Promise<boolean>;
/**
* Set mute state
*/
public abstract setMute(muted: boolean): Promise<void>;
/**
* Get current track info
*/
public abstract getCurrentTrack(): Promise<ITrackInfo | null>;
/**
* Get playback status
*/
public abstract getPlaybackStatus(): Promise<IPlaybackStatus>;
// ============================================================================
// Common Methods
// ============================================================================
/**
* Toggle mute
*/
public async toggleMute(): Promise<boolean> {
const currentMute = await this.getMute();
await this.setMute(!currentMute);
return !currentMute;
}
/**
* Volume up
*/
public async volumeUp(step: number = 5): Promise<number> {
const current = await this.getVolume();
const newVolume = Math.min(100, current + step);
await this.setVolume(newVolume);
return newVolume;
}
/**
* Volume down
*/
public async volumeDown(step: number = 5): Promise<number> {
const current = await this.getVolume();
const newVolume = Math.max(0, current - step);
await this.setVolume(newVolume);
return newVolume;
}
/**
* Get speaker info
*/
public getSpeakerInfo(): ISpeakerInfo {
return {
id: this.id,
name: this.name,
type: 'speaker',
address: this.address,
port: this.port,
status: this.status,
protocol: this._protocol,
roomName: this._roomName,
modelName: this._modelName,
};
}
}