Files
integrations/ts/integrations/mpd/mpd.classes.integration.ts
T

213 lines
9.7 KiB
TypeScript

import type * as shxInterfaces from '@smarthome.exchange/interfaces';
import { BaseIntegration } from '../../core/classes.baseintegration.js';
import type { IIntegrationEntity, IIntegrationRuntime, IIntegrationSetupContext, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
import { MpdClient } from './mpd.classes.client.js';
import { MpdConfigFlow } from './mpd.classes.configflow.js';
import { createMpdDiscoveryDescriptor } from './mpd.discovery.js';
import { MpdMapper } from './mpd.mapper.js';
import type { IMpdConfig, IMpdOutput, IMpdSnapshot } from './mpd.types.js';
export class MpdIntegration extends BaseIntegration<IMpdConfig> {
public readonly domain = 'mpd';
public readonly displayName = 'Music Player Daemon (MPD)';
public readonly status = 'control-runtime' as const;
public readonly discoveryDescriptor = createMpdDiscoveryDescriptor();
public readonly configFlow = new MpdConfigFlow();
public readonly metadata = {
source: 'home-assistant/core',
upstreamPath: 'homeassistant/components/mpd',
upstreamDomain: 'mpd',
integrationType: 'device',
iotClass: 'local_polling',
requirements: ['python-mpd2==3.1.1'],
dependencies: [],
afterDependencies: [],
codeowners: [],
configFlow: true,
documentation: 'https://www.home-assistant.io/integrations/mpd',
};
public async setup(configArg: IMpdConfig, contextArg: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
void contextArg;
return new MpdRuntime(new MpdClient(configArg));
}
public async destroy(): Promise<void> {}
}
export class HomeAssistantMpdIntegration extends MpdIntegration {}
class MpdRuntime implements IIntegrationRuntime {
public domain = 'mpd';
constructor(private readonly client: MpdClient) {}
public async devices(): Promise<shxInterfaces.data.IDeviceDefinition[]> {
return MpdMapper.toDevices(await this.client.getSnapshot());
}
public async entities(): Promise<IIntegrationEntity[]> {
return MpdMapper.toEntities(await this.client.getSnapshot());
}
public async callService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
try {
if (requestArg.domain === 'media_player') {
return await this.callMediaPlayerService(requestArg);
}
if (requestArg.domain === 'mpd') {
return await this.callMpdService(requestArg);
}
return { success: false, error: `Unsupported MPD service domain: ${requestArg.domain}` };
} catch (errorArg) {
return { success: false, error: errorArg instanceof Error ? errorArg.message : String(errorArg) };
}
}
public async destroy(): Promise<void> {
await this.client.destroy();
}
private async callMediaPlayerService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
if (requestArg.service === 'play' || requestArg.service === 'media_play') {
await this.client.play();
return { success: true };
}
if (requestArg.service === 'pause' || requestArg.service === 'media_pause') {
await this.client.pause(true);
return { success: true };
}
if (requestArg.service === 'play_pause' || requestArg.service === 'media_play_pause') {
const snapshot = await this.client.getSnapshot();
await this.client.pause(snapshot.status.state === 'play');
return { success: true };
}
if (requestArg.service === 'stop' || requestArg.service === 'media_stop') {
await this.client.stop();
return { success: true };
}
if (requestArg.service === 'next' || requestArg.service === 'next_track' || requestArg.service === 'media_next_track') {
await this.client.next();
return { success: true };
}
if (requestArg.service === 'previous' || requestArg.service === 'previous_track' || requestArg.service === 'media_previous_track') {
await this.client.previous();
return { success: true };
}
if (requestArg.service === 'volume_set' || requestArg.service === 'set_volume' || requestArg.service === 'volume') {
await this.client.setVolumeLevel(this.numberValue(requestArg.data?.volume_level ?? requestArg.data?.volume, 'MPD volume control requires data.volume_level or data.volume.'));
return { success: true };
}
if (requestArg.service === 'select_source' || requestArg.service === 'source') {
await this.client.selectSource(this.stringValue(requestArg.data?.source ?? requestArg.data?.playlist, 'MPD source control requires data.source or data.playlist.'));
return { success: true };
}
if (requestArg.service === 'play_media') {
await this.client.playMedia(this.stringValue(requestArg.data?.media_content_id ?? requestArg.data?.uri, 'MPD play_media requires data.media_content_id or data.uri.'));
return { success: true };
}
if (requestArg.service === 'enable_output' || requestArg.service === 'disable_output' || requestArg.service === 'toggle_output' || requestArg.service === 'set_output' || requestArg.service === 'output') {
await this.callOutputService(requestArg);
return { success: true };
}
return { success: false, error: `Unsupported MPD media_player service: ${requestArg.service}` };
}
private async callMpdService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
if (requestArg.service === 'snapshot') {
return { success: true, data: await this.client.snapshot() };
}
if (requestArg.service === 'restore') {
const snapshot = requestArg.data?.snapshot as IMpdSnapshot | undefined;
await this.client.restore(snapshot);
return { success: true };
}
if (requestArg.service === 'command') {
const command = this.stringValue(requestArg.data?.command, 'MPD command service requires data.command.');
const args = this.commandArgs(requestArg.data?.args);
return { success: true, data: await this.client.command(command, args) };
}
if (requestArg.service === 'play' || requestArg.service === 'pause' || requestArg.service === 'stop' || requestArg.service === 'next' || requestArg.service === 'previous' || requestArg.service === 'volume_set' || requestArg.service === 'set_volume' || requestArg.service === 'volume' || requestArg.service === 'select_source' || requestArg.service === 'source') {
return this.callMediaPlayerService({ ...requestArg, domain: 'media_player' });
}
if (requestArg.service === 'enable_output' || requestArg.service === 'disable_output' || requestArg.service === 'toggle_output' || requestArg.service === 'set_output' || requestArg.service === 'output') {
await this.callOutputService(requestArg);
return { success: true };
}
return { success: false, error: `Unsupported MPD service: ${requestArg.service}` };
}
private async callOutputService(requestArg: IServiceCallRequest): Promise<void> {
const outputId = await this.outputIdFromRequest(requestArg);
if (requestArg.service === 'toggle_output' || requestArg.service === 'output' && requestArg.data?.enabled === undefined) {
await this.client.toggleOutput(outputId);
return;
}
const enabled = requestArg.service === 'enable_output'
? true
: requestArg.service === 'disable_output'
? false
: this.booleanValue(requestArg.data?.enabled, 'MPD set_output/output requires data.enabled.');
await this.client.setOutput(outputId, enabled);
}
private async outputIdFromRequest(requestArg: IServiceCallRequest): Promise<string | number> {
const direct = requestArg.data?.output_id ?? requestArg.data?.outputId ?? requestArg.data?.id;
if (typeof direct === 'string' || typeof direct === 'number') {
return direct;
}
const targetId = requestArg.target.entityId || requestArg.target.deviceId;
if (!targetId) {
throw new Error('MPD output control requires data.output_id or a target output switch entity.');
}
const snapshot = await this.client.getSnapshot();
const entity = MpdMapper.toEntities(snapshot).find((entityArg) => entityArg.id === targetId || entityArg.deviceId === targetId);
const entityOutputId = entity?.attributes?.mpdOutputId;
if (typeof entityOutputId === 'string' || typeof entityOutputId === 'number') {
return entityOutputId;
}
const output = snapshot.outputs.find((outputArg) => MpdMapper.outputDeviceId(snapshot, outputArg) === targetId || outputEntityId(snapshot, outputArg) === targetId);
if (!output) {
throw new Error(`MPD output target was not found: ${targetId}`);
}
return output.outputid;
}
private commandArgs(valueArg: unknown): Array<string | number | boolean> {
if (valueArg === undefined) {
return [];
}
const values = Array.isArray(valueArg) ? valueArg : [valueArg];
if (values.some((itemArg) => typeof itemArg !== 'string' && typeof itemArg !== 'number' && typeof itemArg !== 'boolean')) {
throw new Error('MPD command args must be strings, numbers, or booleans.');
}
return values as Array<string | number | boolean>;
}
private numberValue(valueArg: unknown, errorArg: string): number {
if (typeof valueArg !== 'number' || !Number.isFinite(valueArg)) {
throw new Error(errorArg);
}
return valueArg;
}
private booleanValue(valueArg: unknown, errorArg: string): boolean {
if (typeof valueArg !== 'boolean') {
throw new Error(errorArg);
}
return valueArg;
}
private stringValue(valueArg: unknown, errorArg: string): string {
if (typeof valueArg !== 'string' || !valueArg) {
throw new Error(errorArg);
}
return valueArg;
}
}
const outputEntityId = (snapshotArg: IMpdSnapshot, outputArg: IMpdOutput): string => {
const serverName = snapshotArg.server.name || snapshotArg.server.host || 'Music Player Daemon';
return `switch.${MpdMapper.slug(serverName)}_${MpdMapper.slug(outputArg.outputname)}_mpd_output`;
};