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(); 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 { 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[] | 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): 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'; } }