Skip to content

Commit

Permalink
feature: predefined groups with volume and media
Browse files Browse the repository at this point in the history
  • Loading branch information
punxaphil committed Sep 12, 2023
1 parent 6c7cbbd commit 0b2b4fc
Show file tree
Hide file tree
Showing 31 changed files with 666 additions and 535 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ entities: # Entities are automatically discovered if you don't supply this setti
- media_player.sonos_bedroom
- media_player.sonos_livingroom


# groups specific
groupsTitle: ''
hideGroupCurrentTrack: true # default is false, which means song/track info for groups will be shown
Expand All @@ -102,6 +101,13 @@ predefinedGroups: # defaults to empty
entities:
- media_player.matrum
- media_player.hall
- name: Kök&Hall
media: Legendary # If you want to start playing a specific favorite when grouping
entities: # Use below format if you want to set the volume of the speakers when grouping
- player: media_player.kok
volume: 10
- player: media_player.hall
volume: 5

# player specific
showVolumeUpAndDownButtons: true # default is false, shows buttons for increasing and decreasing volume
Expand Down
24 changes: 12 additions & 12 deletions src/card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { HomeAssistant } from 'custom-card-helpers';
import { css, html, LitElement } from 'lit';
import { property, state } from 'lit/decorators.js';
import { choose } from 'lit/directives/choose.js';
import Store from './store';
import Store from './model/store';
import { CardConfig, Section } from './types';
import './components/footer';
import './editor/editor';
Expand All @@ -29,7 +29,7 @@ export class Card extends LitElement {
@state() showLoader!: boolean;
@state() loaderTimestamp!: number;
@state() cancelLoader!: boolean;
@state() entityId!: string;
@state() activePlayerId!: string;
render() {
this.createStore();
let height = getHeight(this.config);
Expand Down Expand Up @@ -63,11 +63,11 @@ export class Card extends LitElement {
`;
}
private createStore() {
if (this.entityId) {
this.store = new Store(this.hass, this.config, this.entityId);
if (this.activePlayerId) {
this.store = new Store(this.hass, this.config, this.activePlayerId);
} else {
this.store = new Store(this.hass, this.config);
this.entityId = this.store.entityId;
this.activePlayerId = this.store.activePlayer.id;
}
}
getCardSize() {
Expand All @@ -83,14 +83,14 @@ export class Card extends LitElement {
window.addEventListener(SHOW_SECTION, this.showSectionListener);
window.addEventListener(CALL_MEDIA_STARTED, this.callMediaStartedListener);
window.addEventListener(CALL_MEDIA_DONE, this.callMediaDoneListener);
listenForEntityId(this.entityIdListener);
listenForActivePlayer(this.activePlayerListener);
}

disconnectedCallback() {
window.removeEventListener(SHOW_SECTION, this.showSectionListener);
window.removeEventListener(CALL_MEDIA_STARTED, this.callMediaStartedListener);
window.removeEventListener(CALL_MEDIA_DONE, this.callMediaDoneListener);
stopListeningForEntityId(this.entityIdListener);
stopListeningForActivePlayer(this.activePlayerListener);
super.disconnectedCallback();
}

Expand Down Expand Up @@ -125,10 +125,10 @@ export class Card extends LitElement {
}
};

entityIdListener = (event: Event) => {
activePlayerListener = (event: Event) => {
const newEntityId = (event as CustomEvent).detail.entityId;
if (newEntityId !== this.entityId) {
this.entityId = newEntityId;
if (newEntityId !== this.activePlayerId) {
this.activePlayerId = newEntityId;
this.requestUpdate();
}
};
Expand Down Expand Up @@ -203,12 +203,12 @@ export class Card extends LitElement {
}
}

function listenForEntityId(listener: EventListener) {
function listenForActivePlayer(listener: EventListener) {
window.addEventListener(ACTIVE_PLAYER_EVENT, listener);
const event = new CustomEvent(REQUEST_PLAYER_EVENT, { bubbles: true, composed: true });
window.dispatchEvent(event);
}

function stopListeningForEntityId(listener: EventListener) {
function stopListeningForActivePlayer(listener: EventListener) {
window.removeEventListener(ACTIVE_PLAYER_EVENT, listener);
}
27 changes: 13 additions & 14 deletions src/components/group.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { HomeAssistant } from 'custom-card-helpers';
import { css, html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import { when } from 'lit/directives/when.js';
import Store from '../store';
import { CardConfig, PlayerGroup, Section } from '../types';
import { dispatchActiveEntity, dispatchShowSection, getCurrentTrack, getSpeakerList, isPlaying } from '../utils/utils';
import Store from '../model/store';
import { CardConfig, Section } from '../types';
import { dispatchActivePlayerId, dispatchShowSection, getSpeakerList } from '../utils/utils';
import { REQUEST_PLAYER_EVENT } from '../constants';
import { MediaPlayer } from '../model/media-player';

class Group extends LitElement {
@property() store!: Store;
private hass!: HomeAssistant;
private config!: CardConfig;
private group!: PlayerGroup;
@property() player!: MediaPlayer;
@property() selected = false;

connectedCallback() {
Expand All @@ -26,17 +25,17 @@ class Group extends LitElement {

dispatchEntityIdEvent = () => {
if (this.selected) {
const entityId = this.group.entity;
dispatchActiveEntity(entityId);
const entityId = this.player.id;
dispatchActivePlayerId(entityId);
}
};

render() {
({ config: this.config, hass: this.hass } = this.store);
const currentTrack = this.config.hideGroupCurrentTrack ? '' : getCurrentTrack(this.hass.states[this.group.entity]);
const speakerList = getSpeakerList(this.group, this.config);
this.config = this.store.config;
const currentTrack = this.config.hideGroupCurrentTrack ? '' : this.player.getCurrentTrack();
const speakerList = getSpeakerList(this.player, this.store.predefinedGroups);
this.dispatchEntityIdEvent();
const icon = this.hass.states[this.group.entity].attributes.icon;
const icon = this.player.attributes.icon;
return html`
<mwc-list-item
hasMeta
Expand All @@ -53,7 +52,7 @@ class Group extends LitElement {
</div>
${when(
isPlaying(this.group.state),
this.player.isPlaying(),
() => html`
<div class="bars" slot="meta">
<div></div>
Expand All @@ -70,7 +69,7 @@ class Group extends LitElement {
if (!this.selected) {
this.selected = true;
const newUrl = window.location.href.replace(/#.*/g, '');
window.location.replace(`${newUrl}#${this.group.entity}`);
window.location.replace(`${newUrl}#${this.player.id}`);
this.dispatchEntityIdEvent();
dispatchShowSection(Section.PLAYER);
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/media-browser-icons.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { css, html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import Store from '../store';
import Store from '../model/store';
import { CardConfig, MediaPlayerItem } from '../types';
import { dispatchMediaItemSelected } from '../utils/utils';
import { mediaBrowserTitleStyle } from '../constants';
Expand All @@ -16,7 +16,7 @@ export class MediaBrowserIcons extends LitElement {
private config!: CardConfig;

render() {
({ config: this.config } = this.store);
this.config = this.store.config;

return html`
<style>
Expand Down
4 changes: 2 additions & 2 deletions src/components/media-browser-list.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { css, html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import Store from '../store';
import Store from '../model/store';
import { CardConfig, MediaPlayerItem } from '../types';
import { dispatchMediaItemSelected } from '../utils/utils';
import { listStyle, mediaBrowserTitleStyle } from '../constants';
Expand All @@ -16,7 +16,7 @@ export class MediaBrowserList extends LitElement {
private config!: CardConfig;

render() {
({ config: this.config } = this.store);
this.config = this.store.config;

return html`
<mwc-list multi class="list">
Expand Down
60 changes: 20 additions & 40 deletions src/components/player-controls.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { HomeAssistant } from 'custom-card-helpers';
import { HassEntity } from 'home-assistant-js-websocket';
import { css, html, LitElement } from 'lit';
import { property, state } from 'lit/decorators.js';
import HassService from '../services/hass-service';
import { property } from 'lit/decorators.js';
import MediaControlService from '../services/media-control-service';
import Store from '../store';
import { CardConfig, Members } from '../types';
import { getGroupMembers, isPlaying } from '../utils/utils';
import Store from '../model/store';
import { CardConfig } from '../types';
import {
mdiPauseCircle,
mdiPlayCircle,
Expand All @@ -21,36 +17,20 @@ import {
mdiVolumePlus,
} from '@mdi/js';
import { iconButton } from './icon-button';
import { MediaPlayer } from '../model/media-player';

class PlayerControls extends LitElement {
@property() store!: Store;
private hass!: HomeAssistant;
private config!: CardConfig;
@property()
private entity!: HassEntity;

private isGroup!: boolean;
private entityId!: string;
private activePlayer!: MediaPlayer;
private mediaControlService!: MediaControlService;
private hassService!: HassService;
private members!: Members;
@state() private timerToggleShowAllVolumes!: number;

render() {
({
config: this.config,
hass: this.hass,
entityId: this.entityId,
entity: this.entity,
hassService: this.hassService,
mediaControlService: this.mediaControlService,
} = this.store);
this.members = this.store.groups[this.entityId].members;
this.isGroup = getGroupMembers(this.entity).length > 1;
const playing = isPlaying(this.entity.state);

// ${until(this.getAdditionalSwitches())}
this.config = this.store.config;
this.activePlayer = this.store.activePlayer;
this.mediaControlService = this.store.mediaControlService;

const playing = this.activePlayer.isPlaying();
return html`
<div class="main" id="mediaControls">
<div class="icons">
Expand All @@ -60,25 +40,25 @@ class PlayerControls extends LitElement {
${iconButton(mdiSkipNext, this.next)} ${iconButton(this.repeatIcon(), this.repeat)}
${this.config.showVolumeUpAndDownButtons ? iconButton(mdiVolumePlus, this.volUp) : ''}
</div>
<sonos-volume .store=${this.store} .entityId=${this.entityId} .members=${this.members}></sonos-volume>
<sonos-volume .store=${this.store} .player=${this.activePlayer}></sonos-volume>
</div>
`;
}
private prev = async () => await this.mediaControlService.prev(this.entityId);
private play = async () => await this.mediaControlService.play(this.entityId);
private pause = async () => await this.mediaControlService.pause(this.entityId);
private next = async () => await this.mediaControlService.next(this.entityId);
private shuffle = async () => await this.mediaControlService.shuffle(this.entityId, !this.entity?.attributes.shuffle);
private repeat = async () => await this.mediaControlService.repeat(this.entityId, this.entity?.attributes.repeat);
private volDown = async () => await this.mediaControlService.volumeDown(this.entityId, this.members);
private volUp = async () => await this.mediaControlService.volumeUp(this.entityId, this.members);
private prev = async () => await this.mediaControlService.prev(this.activePlayer);
private play = async () => await this.mediaControlService.play(this.activePlayer);
private pause = async () => await this.mediaControlService.pause(this.activePlayer);
private next = async () => await this.mediaControlService.next(this.activePlayer);
private shuffle = async () => await this.mediaControlService.shuffle(this.activePlayer);
private repeat = async () => await this.mediaControlService.repeat(this.activePlayer);
private volDown = async () => await this.mediaControlService.volumeDown(this.activePlayer);
private volUp = async () => await this.mediaControlService.volumeUp(this.activePlayer);

private shuffleIcon() {
return this.entity?.attributes.shuffle ? mdiShuffleVariant : mdiShuffleDisabled;
return this.activePlayer?.attributes.shuffle ? mdiShuffleVariant : mdiShuffleDisabled;
}

private repeatIcon() {
const repeatState = this.entity?.attributes.repeat;
const repeatState = this.activePlayer?.attributes.repeat;
return repeatState === 'all' ? mdiRepeat : repeatState === 'one' ? mdiRepeatOnce : mdiRepeatOff;
}

Expand Down
23 changes: 11 additions & 12 deletions src/components/player-header.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
import { HomeAssistant } from 'custom-card-helpers';
import { HassEntity } from 'home-assistant-js-websocket';
import { css, html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import Store from '../store';
import Store from '../model/store';
import { CardConfig } from '../types';
import { getCurrentTrack, getSpeakerList } from '../utils/utils';
import { getSpeakerList } from '../utils/utils';
import { MediaPlayer } from '../model/media-player';

class PlayerHeader extends LitElement {
@property() store!: Store;
private hass!: HomeAssistant;
private config!: CardConfig;
private entity!: HassEntity;
private activePlayer!: MediaPlayer;

render() {
({ config: this.config, hass: this.hass, entity: this.entity } = this.store);
const attributes = this.entity.attributes;
const speakerList = getSpeakerList(this.store.groups[this.entity.entity_id], this.config);
this.config = this.store.config;
this.activePlayer = this.store.activePlayer;

const speakerList = getSpeakerList(this.activePlayer, this.store.predefinedGroups);
let song = this.config.labelWhenNoMediaIsSelected ? this.config.labelWhenNoMediaIsSelected : 'No media selected';
if (attributes.media_title) {
song = getCurrentTrack(this.entity);
if (this.activePlayer.attributes.media_title) {
song = this.activePlayer.getCurrentTrack();
}
return html` <div class="info">
<div class="entity">${speakerList}</div>
<div class="song">${song}</div>
<div class="artist-album">${attributes.media_album_name}</div>
<div class="artist-album">${this.activePlayer.attributes.media_album_name}</div>
<sonos-progress .store=${this.store}></sonos-progress>
</div>`;
}
Expand Down
23 changes: 8 additions & 15 deletions src/components/progress.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import { HomeAssistant } from 'custom-card-helpers';
import { HassEntity } from 'home-assistant-js-websocket';
import { css, html, LitElement } from 'lit';
import { property, state } from 'lit/decorators.js';
import Store from '../store';
import { CardConfig } from '../types';
import { isPlaying } from '../utils/utils';
import Store from '../model/store';
import { MediaPlayer } from '../model/media-player';

class Progress extends LitElement {
@property() store!: Store;
private hass!: HomeAssistant;
private config!: CardConfig;
private entityId!: string;
private entity!: HassEntity;
private activePlayer!: MediaPlayer;

@state() private playingProgress!: number;
private tracker?: NodeJS.Timer;
Expand All @@ -25,9 +19,8 @@ class Progress extends LitElement {
}

render() {
({ config: this.config, hass: this.hass, entity: this.entity, entityId: this.entityId } = this.store);
this.entity = this.hass.states[this.entityId];
const mediaDuration = this.entity?.attributes.media_duration || 0;
this.activePlayer = this.store.activePlayer;
const mediaDuration = this.activePlayer?.attributes.media_duration || 0;
const showProgress = mediaDuration > 0;
if (showProgress) {
this.trackProgress();
Expand All @@ -45,9 +38,9 @@ class Progress extends LitElement {
}

trackProgress() {
const position = this.entity?.attributes.media_position || 0;
const playing = isPlaying(this.entity?.state);
const updatedAt = this.entity?.attributes.media_position_updated_at || 0;
const position = this.activePlayer?.attributes.media_position || 0;
const playing = this.activePlayer?.isPlaying();
const updatedAt = this.activePlayer?.attributes.media_position_updated_at || 0;
if (playing) {
this.playingProgress = position + (Date.now() - new Date(updatedAt).getTime()) / 1000.0;
} else {
Expand Down
Loading

0 comments on commit 0b2b4fc

Please sign in to comment.