diff --git a/README.md b/README.md index 3090b61..35d67af 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ sectionButtonIcons: # customize icons for the section buttons volumes: mdi:volume-high mediaTitleRegexToReplace: '.wav?.*' # Regex pattern to replace parts of the media title mediaTitleReplacement: ' radio' # Replacement for the media title regex pattern +inverseGroupMuteState: true # default is false, which means that only if all players are muted, mute icon shows as 'muted'. If set to true, mute icon will show as 'muted' if any player is muted. # groups specific groupsTitle: '' diff --git a/src/components/volume.ts b/src/components/volume.ts index fcea7a8..53321a5 100755 --- a/src/components/volume.ts +++ b/src/components/volume.ts @@ -22,7 +22,8 @@ class Volume extends LitElement { const volume = this.player.getVolume(); const max = this.getMax(volume); - const muteIcon = this.player.isMuted(this.updateMembers) ? mdiVolumeMute : mdiVolumeHigh; + const isMuted = this.updateMembers ? this.player.isGroupMuted() : this.player.isMemberMuted(); + const muteIcon = isMuted ? mdiVolumeMute : mdiVolumeHigh; const disabled = this.player.ignoreVolume; return html` diff --git a/src/editor/advanced-editor.ts b/src/editor/advanced-editor.ts index 35c737b..eaa3b7a 100644 --- a/src/editor/advanced-editor.ts +++ b/src/editor/advanced-editor.ts @@ -157,6 +157,10 @@ export const ADVANCED_SCHEMA = [ name: 'stopInsteadOfPause', selector: { boolean: {} }, }, + { + name: 'inverseGroupMuteState', + selector: { boolean: {} }, + }, ]; class AdvancedEditor extends BaseEditor { diff --git a/src/model/media-player.ts b/src/model/media-player.ts index c4e772a..eaeb477 100644 --- a/src/model/media-player.ts +++ b/src/model/media-player.ts @@ -35,8 +35,15 @@ export class MediaPlayer { return this.state === 'playing'; } - isMuted(checkMembers: boolean): boolean { - return this.attributes.is_volume_muted && (!checkMembers || this.members.every((member) => member.isMuted(false))); + isMemberMuted() { + return this.attributes.is_volume_muted; + } + + isGroupMuted(): boolean { + if (this.config.inverseGroupMuteState) { + return this.members.some((member) => member.isMemberMuted()); + } + return this.members.every((member) => member.isMemberMuted()); } getCurrentTrack() { diff --git a/src/services/media-control-service.ts b/src/services/media-control-service.ts index fb14625..3bc46ba 100644 --- a/src/services/media-control-service.ts +++ b/src/services/media-control-service.ts @@ -127,7 +127,8 @@ export default class MediaControlService { } async toggleMute(mediaPlayer: MediaPlayer, updateMembers = true) { - const muteVolume = !mediaPlayer.isMuted(updateMembers); + const isMuted = updateMembers ? mediaPlayer.isGroupMuted() : mediaPlayer.isMemberMuted(); + const muteVolume = !isMuted; await this.setVolumeMute(mediaPlayer, muteVolume, updateMembers); } diff --git a/src/types.ts b/src/types.ts index de03b18..6f3a546 100644 --- a/src/types.ts +++ b/src/types.ts @@ -100,6 +100,7 @@ export interface CardConfig extends LovelaceCardConfig { mediaTitleRegexToReplace?: string; mediaTitleReplacement?: string; stopInsteadOfPause?: boolean; + inverseGroupMuteState?: boolean; } export interface MediaArtworkOverride { diff --git a/test/model/media-player.test.ts b/test/model/media-player.test.ts index 6b61eed..937a2b6 100644 --- a/test/model/media-player.test.ts +++ b/test/model/media-player.test.ts @@ -10,7 +10,7 @@ describe('MediaPlayer', () => { let config: CardConfig; let mediaPlayer: MediaPlayer; - beforeEach(() => { + function initMediaPlayer(groupSize: number, inverseGroupMuteState = false) { hassEntity1 = { entity_id: 'media_player.first', state: 'playing', @@ -21,7 +21,7 @@ describe('MediaPlayer', () => { media_title: 'Title', media_content_id: 'http://example.com', volume_level: 0.5, - group_members: ['media_player.first'], + group_members: ['media_player.first', 'media_player.second'], }, } as unknown as HassEntity; hassEntity2 = { @@ -29,16 +29,21 @@ describe('MediaPlayer', () => { attributes: { friendly_name: 'Second Player', }, - } as HassEntity; + } as unknown as HassEntity; config = newConfig({ entitiesToIgnoreVolumeLevelFor: ['media_player.ignore'], mediaTitleRegexToReplace: 'Title', mediaTitleReplacement: 'Replaced Title', adjustVolumeRelativeToMainPlayer: true, + inverseGroupMuteState: inverseGroupMuteState, }); - + hassEntity1.attributes.group_members.length = groupSize; mediaPlayer = new MediaPlayer(hassEntity1, config, [hassEntity1, hassEntity2]); + } + + beforeEach(() => { + initMediaPlayer(1); }); it('should initialize correctly', () => { @@ -66,10 +71,22 @@ describe('MediaPlayer', () => { expect(mediaPlayer.isPlaying()).toBe(false); }); - it('should check if is muted correctly', () => { - expect(mediaPlayer.isMuted(false)).toBe(false); + it('should check if member is muted correctly', () => { + expect(mediaPlayer.isMemberMuted()).toBe(false); mediaPlayer.attributes.is_volume_muted = true; - expect(mediaPlayer.isMuted(false)).toBe(true); + expect(mediaPlayer.isMemberMuted()).toBe(true); + }); + + it('should check if group is muted correctly', () => { + initMediaPlayer(2); + expect(mediaPlayer.isGroupMuted()).toBe(false); + mediaPlayer.members[1].attributes.is_volume_muted = true; + expect(mediaPlayer.isGroupMuted()).toBe(false); + mediaPlayer.members.forEach((member) => (member.attributes.is_volume_muted = true)); + expect(mediaPlayer.isGroupMuted()).toBe(true); + initMediaPlayer(2, true); + mediaPlayer.members[1].attributes.is_volume_muted = true; + expect(mediaPlayer.isGroupMuted()).toBe(true); }); it('should get current track correctly', () => {