diff --git a/src/components/volume.ts b/src/components/volume.ts index f77d1918..f2b128df 100755 --- a/src/components/volume.ts +++ b/src/components/volume.ts @@ -19,15 +19,21 @@ class Volume extends LitElement { this.config = this.store.config; this.mediaControlService = this.store.mediaControlService; - const volume = this.getVolume(); + const volume = this.player.getVolume(); const max = volume < 20 && this.config.dynamicVolumeSlider ? 30 : 100; const muteIcon = this.player.isMuted(this.updateMembers) ? mdiVolumeMute : mdiVolumeHigh; + const disabled = this.player.ignoreVolume; return html`
- +
- +
0%
${Math.round(volume)}%
@@ -38,37 +44,9 @@ class Volume extends LitElement { `; } - private getVolume() { - if (this.updateMembers && this.config.adjustVolumeRelativeToMainPlayer) { - const volumes = [ - this.player.attributes.volume_level, - ...this.player.members.map((m) => m.attributes.volume_level), - ]; - return (100 * volumes.reduce((a, b) => a + b, 0)) / volumes.length; - } else { - return 100 * this.getVolumeLevelPlayer().attributes.volume_level; - } - } - - private getVolumeLevelPlayer() { - let volumeLevelPlayer = this.player; - if (this.updateMembers && this.player.members.length && this.config.entitiesToIgnoreVolumeLevelFor) { - const players = [volumeLevelPlayer, ...volumeLevelPlayer.members]; - volumeLevelPlayer = - players.find((p) => { - return !this.config.entitiesToIgnoreVolumeLevelFor?.includes(p.id); - }) ?? volumeLevelPlayer; - } - return volumeLevelPlayer; - } - private async volumeChanged(e: Event) { - const volume = numberFromEvent(e); - return await this.setVolume(volume); - } - - private async setVolume(volume: number) { - return await this.mediaControlService.volumeSet(this.player, volume, this.updateMembers); + const newVolume = numberFromEvent(e); + return await this.mediaControlService.volumeSet(this.player, newVolume, this.updateMembers); } private async mute() { @@ -80,6 +58,9 @@ class Volume extends LitElement { ha-control-slider { --control-slider-color: var(--accent-color); } + ha-control-slider[disabled] { + --control-slider-color: var(--disabled-text-color); + } *[slim] * { --control-slider-thickness: 10px; diff --git a/src/model/media-player.ts b/src/model/media-player.ts index f1d52f0a..c1ee4d7e 100644 --- a/src/model/media-player.ts +++ b/src/model/media-player.ts @@ -9,6 +9,8 @@ export class MediaPlayer { members: MediaPlayer[]; attributes: HassEntity['attributes']; private readonly config: CardConfig; + volumePlayer: MediaPlayer; + ignoreVolume: boolean; constructor(hassEntity: HassEntity, config: CardConfig, mediaPlayerHassEntities?: HassEntity[]) { this.id = hassEntity.entity_id; @@ -16,14 +18,12 @@ export class MediaPlayer { this.name = this.getEntityName(hassEntity, config); this.state = hassEntity.state; this.attributes = hassEntity.attributes; - this.members = mediaPlayerHassEntities ? this.createGroupMembers(hassEntity, mediaPlayerHassEntities) : []; + this.members = mediaPlayerHassEntities ? this.createGroupMembers(hassEntity, mediaPlayerHassEntities) : [this]; + this.volumePlayer = this.determineVolumePlayer(); + this.ignoreVolume = !!this.config.entitiesToIgnoreVolumeLevelFor?.includes(this.volumePlayer.id); } - getPlayer(playerId: string) { - return this.id === playerId ? this : this.getMember(playerId); - } - - private getMember(playerId: string) { + getMember(playerId: string) { return this.members.find((member) => member.id === playerId); } @@ -38,10 +38,10 @@ export class MediaPlayer { isMuted(checkMembers: boolean): boolean { return this.attributes.is_volume_muted && (!checkMembers || this.members.every((member) => member.isMuted(false))); } + getCurrentTrack() { return `${this.attributes.media_artist || ''} - ${this.attributes.media_title || ''}`.replace(/^ - | - $/g, ''); } - private getEntityName(hassEntity: HassEntity, config: CardConfig) { const name = hassEntity.attributes.friendly_name || ''; if (config.entityNameRegexToReplace) { @@ -51,16 +51,34 @@ export class MediaPlayer { } private createGroupMembers(mainHassEntity: HassEntity, mediaPlayerHassEntities: HassEntity[]): MediaPlayer[] { - const groupPlayerIds = getGroupPlayerIds(mainHassEntity); - return mediaPlayerHassEntities - .filter( - (hassEntity) => - groupPlayerIds.includes(hassEntity.entity_id) && mainHassEntity.entity_id !== hassEntity.entity_id, - ) - .map((hassEntity) => new MediaPlayer(hassEntity, this.config)); + return getGroupPlayerIds(mainHassEntity).reduce((players: MediaPlayer[], id) => { + const hassEntity = mediaPlayerHassEntities.find((hassEntity) => hassEntity.entity_id === id); + return hassEntity ? [...players, new MediaPlayer(hassEntity, this.config)] : players; + }, []); + } + + private determineVolumePlayer() { + let find; + if (this.members.length > 1 && this.config.entitiesToIgnoreVolumeLevelFor) { + find = this.members.find((p) => { + return !this.config.entitiesToIgnoreVolumeLevelFor?.includes(p.id); + }); + } + return find ?? this; + } + + getVolume() { + if (this.members.length > 1 && this.config.adjustVolumeRelativeToMainPlayer) { + return this.getAverageVolume(); + } else { + return 100 * this.volumePlayer.attributes.volume_level; + } } - isGrouped() { - return this.members.length > 0; + private getAverageVolume() { + const volumes = this.members + .filter((m) => !this.config.entitiesToIgnoreVolumeLevelFor?.includes(m.id)) + .map((m) => m.attributes.volume_level); + return (100 * volumes.reduce((a, b) => a + b, 0)) / volumes.length; } } diff --git a/src/model/store.ts b/src/model/store.ts index 6b7a2f8f..0b11e650 100644 --- a/src/model/store.ts +++ b/src/model/store.ts @@ -38,10 +38,7 @@ export default class Store { const mediaPlayerHassEntities = this.getMediaPlayerHassEntities(this.hass); this.allGroups = this.createPlayerGroups(mediaPlayerHassEntities); this.allMediaPlayers = this.allGroups - .reduce( - (previousValue: MediaPlayer[], currentValue) => [...previousValue, currentValue, ...currentValue.members], - [], - ) + .reduce((previousValue: MediaPlayer[], currentValue) => [...previousValue, ...currentValue.members], []) .sort((a, b) => a.name.localeCompare(b.name)); this.activePlayer = this.determineActivePlayer(activePlayerId); this.hassService = new HassService(this.hass, currentSection, card, config); @@ -169,7 +166,7 @@ export default class Store { private determineActivePlayer(activePlayerId?: string): MediaPlayer { const playerId = activePlayerId || this.config.entityId || this.getActivePlayerFromUrl(); return ( - this.allGroups.find((group) => group.getPlayer(playerId) !== undefined) || + this.allGroups.find((group) => group.getMember(playerId) !== undefined) || this.allGroups.find((group) => group.isPlaying()) || this.allGroups[0] ); diff --git a/src/sections/volumes.ts b/src/sections/volumes.ts index 23125a73..0c9d14fa 100755 --- a/src/sections/volumes.ts +++ b/src/sections/volumes.ts @@ -28,8 +28,8 @@ class Volumes extends LitElement { const members = this.activePlayer.members; return html` - ${when(members.length, () => this.volumeWithName(this.activePlayer))} - ${[this.activePlayer, ...members].map((member) => this.volumeWithName(member, false))} + ${when(members.length > 1, () => this.volumeWithName(this.activePlayer))} + ${members.map((member) => this.volumeWithName(member, false))} `; } @@ -48,9 +48,19 @@ class Volumes extends LitElement {
${name}
- + - + this.toggleShowSwitches(player)} diff --git a/src/services/media-control-service.ts b/src/services/media-control-service.ts index 3c24663c..a1ad58e0 100644 --- a/src/services/media-control-service.ts +++ b/src/services/media-control-service.ts @@ -1,4 +1,4 @@ -import { CardConfig, MediaPlayerItem, PredefinedGroup } from '../types'; +import { CalculateVolume, CardConfig, MediaPlayerItem, PredefinedGroup } from '../types'; import HassService from './hass-service'; import { MediaPlayer } from '../model/media-player'; @@ -28,7 +28,7 @@ export default class MediaControlService { for (const pgp of pg.entities) { const volume = pgp.volume ?? pg.volume; if (volume) { - await this.volumeSet(pgp.player, volume, false); + await this.volumeSetSinglePlayer(pgp.player, volume); } if (pg.unmuteWhenGrouped) { await this.setVolumeMute(pgp.player, false, false); @@ -39,50 +39,90 @@ export default class MediaControlService { } } - async volumeDown(mediaPlayer: MediaPlayer, updateMembers = true) { + async volumeDown(mainPlayer: MediaPlayer, updateMembers = true) { + await this.volumeStep(mainPlayer, updateMembers, this.getStepDownVolume, 'volume_down'); + } + + async volumeUp(mainPlayer: MediaPlayer, updateMembers = true) { + await this.volumeStep(mainPlayer, updateMembers, this.getStepUpVolume, 'volume_up'); + } + + private async volumeStep( + mainPlayer: MediaPlayer, + updateMembers: boolean, + calculateVolume: (member: MediaPlayer, volumeStepSize: number) => number, + stepDirection: string, + ) { if (this.config.volumeStepSize) { - const volume = mediaPlayer.attributes.volume_level * 100; - const newVolume = Math.max(0, volume - this.config.volumeStepSize); - await this.volumeSet(mediaPlayer, newVolume, updateMembers); - return; + await this.volumeWithStepSize(mainPlayer, updateMembers, this.config.volumeStepSize, calculateVolume); } else { - await this.hassService.callMediaService('volume_down', { entity_id: mediaPlayer.id }); - if (updateMembers) { - for (const member of mediaPlayer.members) { - await this.hassService.callMediaService('volume_down', { entity_id: member.id }); - } + await this.volumeDefaultStep(mainPlayer, updateMembers, stepDirection); + } + } + + private async volumeWithStepSize( + mainPlayer: MediaPlayer, + updateMembers: boolean, + volumeStepSize: number, + calculateVolume: CalculateVolume, + ) { + for (const member of mainPlayer.members) { + if (mainPlayer.id === member.id || updateMembers) { + const newVolume = calculateVolume(member, volumeStepSize); + await this.volumeSetSinglePlayer(member, newVolume); } } } - async volumeUp(mediaPlayer: MediaPlayer, updateMembers = true) { - if (this.config.volumeStepSize) { - const volume = mediaPlayer.attributes.volume_level * 100; - const newVolume = Math.min(100, volume + this.config.volumeStepSize); - await this.volumeSet(mediaPlayer, newVolume, updateMembers); - } else { - await this.hassService.callMediaService('volume_up', { entity_id: mediaPlayer.id }); - if (updateMembers) { - for (const member of mediaPlayer.members) { - await this.hassService.callMediaService('volume_up', { entity_id: member.id }); + private getStepDownVolume(member: MediaPlayer, volumeStepSize: number) { + return Math.max(0, member.getVolume() - volumeStepSize); + } + + private getStepUpVolume(member: MediaPlayer, stepSize: number) { + return Math.min(100, member.getVolume() + stepSize); + } + + private async volumeDefaultStep(mainPlayer: MediaPlayer, updateMembers: boolean, stepDirection: string) { + for (const member of mainPlayer.members) { + if (mainPlayer.id === member.id || updateMembers) { + if (!member.ignoreVolume) { + await this.hassService.callMediaService(stepDirection, { entity_id: member.id }); } } } } - async volumeSet(mediaPlayer: MediaPlayer, volume: number, updateMembers = true) { - let volume_level = volume / 100; - - await this.hassService.callMediaService('volume_set', { entity_id: mediaPlayer.id, volume_level: volume_level }); - const relativeVolumeChange = volume_level - mediaPlayer.attributes.volume_level; + async volumeSet(player: MediaPlayer, volume: number, updateMembers: boolean) { if (updateMembers) { - for (const member of mediaPlayer.members) { - if (this.config.adjustVolumeRelativeToMainPlayer) { - volume_level = member.attributes.volume_level + relativeVolumeChange; - volume_level = Math.min(1, Math.max(0, volume_level)); + return await this.volumeSetGroup(player, volume); + } else { + return await this.volumeSetSinglePlayer(player, volume); + } + } + private async volumeSetGroup(player: MediaPlayer, volumePercent: number) { + let relativeVolumeChange: number | undefined; + if (this.config.adjustVolumeRelativeToMainPlayer) { + relativeVolumeChange = volumePercent - player.getVolume(); + } + + await Promise.all( + player.members.map((member) => { + let memberVolume = volumePercent; + if (relativeVolumeChange !== undefined) { + if (this.config.adjustVolumeRelativeToMainPlayer) { + memberVolume = member.getVolume() + relativeVolumeChange; + memberVolume = Math.min(100, Math.max(0, memberVolume)); + } } - await this.hassService.callMediaService('volume_set', { entity_id: member.id, volume_level }); - } + this.volumeSetSinglePlayer(member, memberVolume); + }), + ); + } + + async volumeSetSinglePlayer(player: MediaPlayer, volumePercent: number) { + if (!player.ignoreVolume) { + const volume = volumePercent / 100; + await this.hassService.callMediaService('volume_set', { entity_id: player.id, volume_level: volume }); } } @@ -92,9 +132,8 @@ export default class MediaControlService { } async setVolumeMute(mediaPlayer: MediaPlayer, muteVolume: boolean, updateMembers = true) { - await this.hassService.callMediaService('volume_mute', { entity_id: mediaPlayer.id, is_volume_muted: muteVolume }); - if (updateMembers) { - for (const member of mediaPlayer.members) { + for (const member of mediaPlayer.members) { + if (mediaPlayer.id === member.id || updateMembers) { await this.hassService.callMediaService('volume_mute', { entity_id: member.id, is_volume_muted: muteVolume }); } } diff --git a/src/types.ts b/src/types.ts index 72f681bf..d5207764 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,6 +18,8 @@ export enum Section { export type ConfigPredefinedGroupPlayer = PredefinedGroupPlayer; export type ConfigPredefinedGroup = PredefinedGroup; +export type CalculateVolume = (member: MediaPlayer, volumeStepSize: number) => number; + export interface CardConfig extends LovelaceCardConfig { sections?: Section[]; showVolumeUpAndDownButtons?: boolean; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 259a089a..dd581b20 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -4,7 +4,7 @@ import { ACTIVE_PLAYER_EVENT, ACTIVE_PLAYER_EVENT_INTERNAL } from '../constants' import { MediaPlayer } from '../model/media-player'; export function getSpeakerList(mainPlayer: MediaPlayer, predefinedGroups: PredefinedGroup[] = []) { - const playerIds = [mainPlayer.id, ...mainPlayer.members.map((member) => member.id)].sort(); + const playerIds = mainPlayer.members.map((member) => member.id).sort(); if (predefinedGroups?.length) { const found = predefinedGroups.find( (pg) => @@ -17,7 +17,7 @@ export function getSpeakerList(mainPlayer: MediaPlayer, predefinedGroups: Predef return found.name; } } - return [mainPlayer.name, ...mainPlayer.members.map((member) => member.name)].join(' + '); + return mainPlayer.members.map((member) => member.name).join(' + '); } export function dispatchActivePlayerId(playerId: string, config: CardConfig, element: Element) {