feat(devicemanager): Introduce a UniversalDevice architecture with composable Feature system; add extensive new device/protocol support and discovery/refactors
This commit is contained in:
216
ts/speaker/speaker.classes.speaker.ts
Normal file
216
ts/speaker/speaker.classes.speaker.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user