-
+
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) {