343 lines
18 KiB
TypeScript
343 lines
18 KiB
TypeScript
import * as plugins from '../../plugins.js';
|
|
import type { IIntegrationEntity } from '../../core/types.js';
|
|
import type {
|
|
IYamahaMusiccastCapability,
|
|
IYamahaMusiccastRangeStep,
|
|
IYamahaMusiccastSnapshot,
|
|
IYamahaMusiccastZoneState,
|
|
} from './yamaha_musiccast.types.js';
|
|
|
|
export class YamahaMusiccastMapper {
|
|
public static toDevices(snapshotArg: IYamahaMusiccastSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
|
|
const updatedAt = new Date().toISOString();
|
|
return snapshotArg.zones.map((zoneArg) => {
|
|
const capabilities = this.capabilitiesForZone(zoneArg);
|
|
const volumePercent = this.volumePercent(zoneArg);
|
|
const features: plugins.shxInterfaces.data.IDeviceFeature[] = [
|
|
{ id: 'power', capability: 'media', name: 'Power', readable: true, writable: true },
|
|
{ id: 'playback', capability: 'media', name: 'Playback', readable: true, writable: true },
|
|
{ id: 'source', capability: 'media', name: 'Source', readable: true, writable: true },
|
|
{ id: 'volume', capability: 'media', name: 'Volume', readable: true, writable: true, unit: '%' },
|
|
{ id: 'muted', capability: 'media', name: 'Muted', readable: true, writable: true },
|
|
];
|
|
const state: plugins.shxInterfaces.data.IDeviceState[] = [
|
|
{ featureId: 'power', value: this.powerState(zoneArg), updatedAt },
|
|
{ featureId: 'playback', value: this.mediaState(snapshotArg, zoneArg), updatedAt },
|
|
{ featureId: 'source', value: zoneArg.input || null, updatedAt },
|
|
{ featureId: 'volume', value: typeof volumePercent === 'number' ? volumePercent : null, updatedAt },
|
|
{ featureId: 'muted', value: typeof zoneArg.muted === 'boolean' ? zoneArg.muted : null, updatedAt },
|
|
];
|
|
|
|
if (zoneArg.soundProgram || zoneArg.soundProgramList?.length) {
|
|
features.push({ id: 'sound_program', capability: 'media', name: 'Sound Program', readable: true, writable: true });
|
|
state.push({ featureId: 'sound_program', value: zoneArg.soundProgram || null, updatedAt });
|
|
}
|
|
|
|
for (const capability of capabilities) {
|
|
features.push({
|
|
id: `capability_${capability.id}`,
|
|
capability: capability.type === 'switch' ? 'switch' : 'media',
|
|
name: capability.name,
|
|
readable: true,
|
|
writable: true,
|
|
unit: capability.type === 'number' ? this.unitForCapability(capability) : undefined,
|
|
});
|
|
state.push({ featureId: `capability_${capability.id}`, value: this.deviceStateValue(capability.current), updatedAt });
|
|
}
|
|
|
|
return {
|
|
id: this.deviceId(snapshotArg, zoneArg),
|
|
integrationDomain: 'yamaha_musiccast',
|
|
name: this.deviceName(snapshotArg, zoneArg),
|
|
protocol: 'http',
|
|
manufacturer: 'Yamaha Corporation',
|
|
model: snapshotArg.deviceInfo.model_name,
|
|
online: zoneArg.available !== false,
|
|
features,
|
|
state,
|
|
metadata: {
|
|
host: snapshotArg.networkStatus?.ip_address,
|
|
zone: zoneArg.zone,
|
|
systemId: snapshotArg.deviceInfo.system_id,
|
|
deviceId: snapshotArg.deviceInfo.device_id,
|
|
serialNumber: snapshotArg.deviceInfo.serial_number,
|
|
systemVersion: snapshotArg.deviceInfo.system_version,
|
|
apiVersion: snapshotArg.deviceInfo.api_version,
|
|
networkName: snapshotArg.networkStatus?.network_name,
|
|
macAddresses: snapshotArg.networkStatus?.mac_address,
|
|
viaDeviceId: zoneArg.zone === 'main' ? undefined : this.mainDeviceId(snapshotArg),
|
|
},
|
|
};
|
|
});
|
|
}
|
|
|
|
public static toEntities(snapshotArg: IYamahaMusiccastSnapshot): IIntegrationEntity[] {
|
|
const entities: IIntegrationEntity[] = [];
|
|
for (const zone of snapshotArg.zones) {
|
|
const base = this.entityBase(snapshotArg, zone);
|
|
entities.push({
|
|
id: `media_player.${base}`,
|
|
uniqueId: `yamaha_musiccast_${this.uniqueBase(snapshotArg)}_${this.slug(zone.zone)}`,
|
|
integrationDomain: 'yamaha_musiccast',
|
|
deviceId: this.deviceId(snapshotArg, zone),
|
|
platform: 'media_player',
|
|
name: this.deviceName(snapshotArg, zone),
|
|
state: this.mediaState(snapshotArg, zone),
|
|
attributes: {
|
|
deviceClass: 'speaker',
|
|
zone: zone.zone,
|
|
power: zone.power,
|
|
volumeLevel: this.volumeLevel(zone),
|
|
volume: zone.volume,
|
|
minVolume: zone.minVolume,
|
|
maxVolume: zone.maxVolume,
|
|
isVolumeMuted: zone.muted,
|
|
source: this.sourceLabel(snapshotArg, zone.input, zone),
|
|
sourceId: zone.input,
|
|
sourceList: this.sourceList(snapshotArg, zone),
|
|
soundMode: zone.soundProgram,
|
|
soundModeList: zone.soundProgramList,
|
|
mediaTitle: this.mediaTitle(snapshotArg, zone),
|
|
mediaArtist: this.mediaArtist(snapshotArg, zone),
|
|
mediaAlbumName: snapshotArg.netusb?.album,
|
|
mediaImageUrl: snapshotArg.netusb?.albumart_url,
|
|
mediaDuration: snapshotArg.netusb?.total_time,
|
|
mediaPosition: snapshotArg.netusb?.play_time,
|
|
repeat: this.isNetusbZone(snapshotArg, zone) ? snapshotArg.netusb?.repeat : undefined,
|
|
shuffle: this.isNetusbZone(snapshotArg, zone) ? snapshotArg.netusb?.shuffle === 'on' : undefined,
|
|
groupId: snapshotArg.distribution?.group_id,
|
|
groupName: snapshotArg.distribution?.group_name,
|
|
groupRole: snapshotArg.distribution?.role,
|
|
},
|
|
available: zone.available !== false,
|
|
});
|
|
|
|
for (const capability of this.capabilitiesForZone(zone)) {
|
|
const entityBase = `${base}_${this.slug(capability.id)}`;
|
|
entities.push({
|
|
id: `${capability.type}.${entityBase}`,
|
|
uniqueId: `yamaha_musiccast_${this.uniqueBase(snapshotArg)}_${this.slug(zone.zone)}_${this.slug(capability.id)}`,
|
|
integrationDomain: 'yamaha_musiccast',
|
|
deviceId: this.deviceId(snapshotArg, zone),
|
|
platform: capability.type,
|
|
name: `${this.deviceName(snapshotArg, zone)} ${capability.name}`,
|
|
state: this.entityState(capability),
|
|
attributes: this.capabilityAttributes(capability, zone),
|
|
available: zone.available !== false,
|
|
});
|
|
}
|
|
}
|
|
return entities;
|
|
}
|
|
|
|
public static capabilitiesForZone(zoneArg: IYamahaMusiccastZoneState): IYamahaMusiccastCapability[] {
|
|
const explicit = zoneArg.capabilities || [];
|
|
const generated: IYamahaMusiccastCapability[] = [];
|
|
|
|
this.pushSwitch(generated, zoneArg, 'enhancer', 'Enhancer', zoneArg.enhancer);
|
|
this.pushSwitch(generated, zoneArg, 'pure_direct', 'Pure Direct', zoneArg.pureDirect);
|
|
this.pushSwitch(generated, zoneArg, 'extra_bass', 'Extra Bass', zoneArg.extraBass);
|
|
this.pushSwitch(generated, zoneArg, 'bass_extension', 'Bass Extension', zoneArg.bassExtension);
|
|
this.pushSwitch(generated, zoneArg, 'adaptive_drc', 'Adaptive DRC', zoneArg.adaptiveDrc);
|
|
this.pushSwitch(generated, zoneArg, 'clear_voice', 'Clear Voice', zoneArg.clearVoice);
|
|
this.pushSwitch(generated, zoneArg, 'surround_3d', '3D Surround', zoneArg.surround3d);
|
|
this.pushSwitch(generated, zoneArg, 'mono', 'Mono', zoneArg.mono);
|
|
|
|
this.pushSelect(generated, zoneArg, 'sleep', 'Sleep Timer', zoneArg.sleep === 0 ? 'off' : typeof zoneArg.sleep === 'number' ? `${zoneArg.sleep}_min` : undefined, ['off', '30_min', '60_min', '90_min', '120_min']);
|
|
this.pushSelect(generated, zoneArg, 'tone_control_mode', 'Tone Control Mode', zoneArg.toneControl?.mode, zoneArg.toneControlModeList);
|
|
this.pushSelect(generated, zoneArg, 'equalizer_mode', 'Equalizer Mode', zoneArg.equalizer?.mode, zoneArg.equalizerModeList);
|
|
this.pushSelect(generated, zoneArg, 'surr_decoder_type', 'Surround Decoder Type', zoneArg.surrDecoderType, zoneArg.surrDecoderTypeList);
|
|
this.pushSelect(generated, zoneArg, 'link_control', 'Link Control', zoneArg.linkControl, zoneArg.linkControlList);
|
|
this.pushSelect(generated, zoneArg, 'link_audio_delay', 'Link Audio Delay', zoneArg.linkAudioDelay, zoneArg.linkAudioDelayList);
|
|
this.pushSelect(generated, zoneArg, 'link_audio_quality', 'Link Audio Quality', zoneArg.linkAudioQuality, zoneArg.linkAudioQualityList);
|
|
|
|
this.pushNumber(generated, zoneArg, 'tone_control_bass', 'Tone Control Bass', zoneArg.toneControl?.bass, this.range(zoneArg, 'tone_control'));
|
|
this.pushNumber(generated, zoneArg, 'tone_control_treble', 'Tone Control Treble', zoneArg.toneControl?.treble, this.range(zoneArg, 'tone_control'));
|
|
this.pushNumber(generated, zoneArg, 'equalizer_low', 'Equalizer Low', zoneArg.equalizer?.low, this.range(zoneArg, 'equalizer'));
|
|
this.pushNumber(generated, zoneArg, 'equalizer_mid', 'Equalizer Mid', zoneArg.equalizer?.mid, this.range(zoneArg, 'equalizer'));
|
|
this.pushNumber(generated, zoneArg, 'equalizer_high', 'Equalizer High', zoneArg.equalizer?.high, this.range(zoneArg, 'equalizer'));
|
|
this.pushNumber(generated, zoneArg, 'balance', 'Balance', zoneArg.balance, this.range(zoneArg, 'balance'));
|
|
this.pushNumber(generated, zoneArg, 'dialogue_level', 'Dialogue Level', zoneArg.dialogueLevel, this.range(zoneArg, 'dialogue_level'));
|
|
this.pushNumber(generated, zoneArg, 'dialogue_lift', 'Dialogue Lift', zoneArg.dialogueLift, this.range(zoneArg, 'dialogue_lift'));
|
|
this.pushNumber(generated, zoneArg, 'dts_dialogue_control', 'DTS Dialogue Control', zoneArg.dtsDialogueControl, this.range(zoneArg, 'dts_dialogue_control'));
|
|
this.pushNumber(generated, zoneArg, 'subwoofer_volume', 'Subwoofer Volume', zoneArg.subwooferVolume, this.range(zoneArg, 'subwoofer_volume'));
|
|
|
|
const byId = new Map<string, IYamahaMusiccastCapability>();
|
|
for (const capability of [...generated, ...explicit]) {
|
|
byId.set(capability.id, { category: 'config', zoneId: zoneArg.zone, ...capability });
|
|
}
|
|
return [...byId.values()];
|
|
}
|
|
|
|
private static pushSwitch(listArg: IYamahaMusiccastCapability[], zoneArg: IYamahaMusiccastZoneState, idArg: string, nameArg: string, valueArg: boolean | undefined): void {
|
|
if (typeof valueArg === 'boolean') {
|
|
listArg.push({ id: idArg, name: nameArg, type: 'switch', zoneId: zoneArg.zone, current: valueArg, category: 'config' });
|
|
}
|
|
}
|
|
|
|
private static pushSelect(listArg: IYamahaMusiccastCapability[], zoneArg: IYamahaMusiccastZoneState, idArg: string, nameArg: string, valueArg: string | undefined, optionsArg: string[] | undefined): void {
|
|
if (valueArg !== undefined || optionsArg?.length) {
|
|
listArg.push({ id: idArg, name: nameArg, type: 'select', zoneId: zoneArg.zone, current: valueArg ?? null, options: optionsArg, category: 'config' });
|
|
}
|
|
}
|
|
|
|
private static pushNumber(listArg: IYamahaMusiccastCapability[], zoneArg: IYamahaMusiccastZoneState, idArg: string, nameArg: string, valueArg: number | undefined, rangeArg: IYamahaMusiccastRangeStep | undefined): void {
|
|
if (typeof valueArg === 'number' || rangeArg) {
|
|
listArg.push({ id: idArg, name: nameArg, type: 'number', zoneId: zoneArg.zone, current: valueArg ?? null, range: rangeArg, category: 'config' });
|
|
}
|
|
}
|
|
|
|
private static entityState(capabilityArg: IYamahaMusiccastCapability): unknown {
|
|
if (capabilityArg.type === 'switch') {
|
|
return Boolean(capabilityArg.current);
|
|
}
|
|
return capabilityArg.current ?? null;
|
|
}
|
|
|
|
private static capabilityAttributes(capabilityArg: IYamahaMusiccastCapability, zoneArg: IYamahaMusiccastZoneState): Record<string, unknown> {
|
|
return {
|
|
zone: zoneArg.zone,
|
|
capabilityId: capabilityArg.id,
|
|
category: capabilityArg.category,
|
|
options: this.optionValues(capabilityArg.options),
|
|
nativeMinValue: capabilityArg.range?.min,
|
|
nativeMaxValue: capabilityArg.range?.max,
|
|
nativeStep: capabilityArg.range?.step,
|
|
};
|
|
}
|
|
|
|
private static optionValues(optionsArg: Record<string, string> | string[] | undefined): string[] | undefined {
|
|
if (!optionsArg) {
|
|
return undefined;
|
|
}
|
|
return Array.isArray(optionsArg) ? optionsArg : Object.values(optionsArg);
|
|
}
|
|
|
|
private static mediaState(snapshotArg: IYamahaMusiccastSnapshot, zoneArg: IYamahaMusiccastZoneState): string {
|
|
if (this.powerState(zoneArg) === 'off') {
|
|
return 'off';
|
|
}
|
|
const explicitState = zoneArg.state?.toLowerCase();
|
|
if (explicitState === 'playing' || explicitState === 'paused' || explicitState === 'idle') {
|
|
return explicitState;
|
|
}
|
|
if (this.isNetusbZone(snapshotArg, zoneArg)) {
|
|
if (snapshotArg.netusb?.playback === 'pause') {
|
|
return 'paused';
|
|
}
|
|
if (snapshotArg.netusb?.playback === 'stop') {
|
|
return 'idle';
|
|
}
|
|
}
|
|
return 'playing';
|
|
}
|
|
|
|
private static powerState(zoneArg: IYamahaMusiccastZoneState): string {
|
|
const power = zoneArg.power?.toLowerCase();
|
|
return power === 'on' ? 'on' : 'off';
|
|
}
|
|
|
|
private static volumeLevel(zoneArg: IYamahaMusiccastZoneState): number | undefined {
|
|
if (typeof zoneArg.volumeLevel === 'number') {
|
|
return Math.max(0, Math.min(1, zoneArg.volumeLevel));
|
|
}
|
|
if (typeof zoneArg.volume === 'number' && typeof zoneArg.minVolume === 'number' && typeof zoneArg.maxVolume === 'number' && zoneArg.maxVolume > zoneArg.minVolume) {
|
|
return Math.max(0, Math.min(1, (zoneArg.volume - zoneArg.minVolume) / (zoneArg.maxVolume - zoneArg.minVolume)));
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
private static volumePercent(zoneArg: IYamahaMusiccastZoneState): number | undefined {
|
|
const level = this.volumeLevel(zoneArg);
|
|
return typeof level === 'number' ? Math.round(level * 100) : undefined;
|
|
}
|
|
|
|
private static sourceList(snapshotArg: IYamahaMusiccastSnapshot, zoneArg: IYamahaMusiccastZoneState): string[] | undefined {
|
|
return zoneArg.inputList?.map((inputArg) => this.sourceLabel(snapshotArg, inputArg, zoneArg)).filter((valueArg): valueArg is string => Boolean(valueArg));
|
|
}
|
|
|
|
private static sourceLabel(snapshotArg: IYamahaMusiccastSnapshot, inputArg: string | undefined, zoneArg?: IYamahaMusiccastZoneState): string | undefined {
|
|
if (!inputArg) {
|
|
return undefined;
|
|
}
|
|
return zoneArg?.sourceMap?.[inputArg] || snapshotArg.inputNames?.[inputArg] || (zoneArg?.input === inputArg ? zoneArg.inputText : undefined) || this.titleize(inputArg);
|
|
}
|
|
|
|
private static mediaTitle(snapshotArg: IYamahaMusiccastSnapshot, zoneArg: IYamahaMusiccastZoneState): string | undefined {
|
|
if (this.isNetusbZone(snapshotArg, zoneArg)) {
|
|
return snapshotArg.netusb?.track || this.sourceLabel(snapshotArg, zoneArg.input, zoneArg);
|
|
}
|
|
if (zoneArg.input === 'tuner') {
|
|
return snapshotArg.tuner?.rds?.radio_text_a || snapshotArg.tuner?.rds?.program_service || this.sourceLabel(snapshotArg, zoneArg.input, zoneArg);
|
|
}
|
|
return this.sourceLabel(snapshotArg, zoneArg.input, zoneArg);
|
|
}
|
|
|
|
private static mediaArtist(snapshotArg: IYamahaMusiccastSnapshot, zoneArg: IYamahaMusiccastZoneState): string | undefined {
|
|
if (this.isNetusbZone(snapshotArg, zoneArg)) {
|
|
return snapshotArg.netusb?.artist;
|
|
}
|
|
if (zoneArg.input === 'tuner') {
|
|
return snapshotArg.tuner?.rds?.radio_text_b || snapshotArg.tuner?.band;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
private static isNetusbZone(snapshotArg: IYamahaMusiccastSnapshot, zoneArg: IYamahaMusiccastZoneState): boolean {
|
|
return Boolean(snapshotArg.netusb?.input && snapshotArg.netusb.input === zoneArg.input);
|
|
}
|
|
|
|
private static deviceName(snapshotArg: IYamahaMusiccastSnapshot, zoneArg: IYamahaMusiccastZoneState): string {
|
|
const receiver = this.receiverName(snapshotArg);
|
|
if (zoneArg.zone === 'main') {
|
|
return receiver;
|
|
}
|
|
return `${receiver} ${zoneArg.name || this.titleize(zoneArg.zone)}`;
|
|
}
|
|
|
|
private static receiverName(snapshotArg: IYamahaMusiccastSnapshot): string {
|
|
return snapshotArg.networkStatus?.network_name || snapshotArg.deviceInfo.model_name || 'Yamaha MusicCast';
|
|
}
|
|
|
|
private static mainDeviceId(snapshotArg: IYamahaMusiccastSnapshot): string {
|
|
return this.deviceId(snapshotArg, { zone: 'main' });
|
|
}
|
|
|
|
private static deviceId(snapshotArg: IYamahaMusiccastSnapshot, zoneArg: Pick<IYamahaMusiccastZoneState, 'zone'>): string {
|
|
const suffix = zoneArg.zone === 'main' ? '' : `.${this.slug(zoneArg.zone)}`;
|
|
return `yamaha_musiccast.player.${this.uniqueBase(snapshotArg)}${suffix}`;
|
|
}
|
|
|
|
private static entityBase(snapshotArg: IYamahaMusiccastSnapshot, zoneArg: IYamahaMusiccastZoneState): string {
|
|
const suffix = zoneArg.zone === 'main' ? '' : `_${this.slug(zoneArg.zone)}`;
|
|
return `${this.slug(this.receiverName(snapshotArg))}${suffix}`;
|
|
}
|
|
|
|
private static uniqueBase(snapshotArg: IYamahaMusiccastSnapshot): string {
|
|
return this.slug(snapshotArg.deviceInfo.system_id || snapshotArg.deviceInfo.device_id || snapshotArg.deviceInfo.serial_number || snapshotArg.deviceInfo.model_name || this.receiverName(snapshotArg));
|
|
}
|
|
|
|
private static range(zoneArg: IYamahaMusiccastZoneState, idArg: string): IYamahaMusiccastRangeStep | undefined {
|
|
return zoneArg.rangeStep?.find((rangeArg) => rangeArg.id === idArg);
|
|
}
|
|
|
|
private static unitForCapability(capabilityArg: IYamahaMusiccastCapability): string | undefined {
|
|
return capabilityArg.id.includes('volume') ? 'level' : undefined;
|
|
}
|
|
|
|
private static deviceStateValue(valueArg: unknown): plugins.shxInterfaces.data.TDeviceStateValue {
|
|
if (typeof valueArg === 'string' || typeof valueArg === 'number' || typeof valueArg === 'boolean' || valueArg === null) {
|
|
return valueArg;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static titleize(valueArg: string): string {
|
|
return valueArg.replace(/_/g, ' ').replace(/\b\w/g, (matchArg) => matchArg.toUpperCase());
|
|
}
|
|
|
|
private static slug(valueArg: string | undefined): string {
|
|
return (valueArg || 'yamaha_musiccast').toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'yamaha_musiccast';
|
|
}
|
|
}
|