class JukeboxCard extends HTMLElement { set hass(hass) { if (!this.content) { this._hassObservers = []; this.appendChild(getStyle()); const card = document.createElement('ha-card'); this.content = document.createElement('div'); card.appendChild(this.content); this.appendChild(card); this.content.appendChild(this.buildSpeakerSwitches(hass)); this.content.appendChild(this.buildVolumeSlider()); this.content.appendChild(this.buildStationList()); } this._hass = hass; this._hassObservers.forEach(listener => listener(hass)); } get hass() { return this._hass; } buildSpeakerSwitches(hass) { this._tabs = document.createElement('paper-tabs'); this._tabs.setAttribute('scrollable', true); this._tabs.addEventListener('iron-activate', (e) => this.onSpeakerSelect(e.detail.item.entityId)); this.config.entities.forEach(entityId => { this._tabs.appendChild(this.buildSpeakerSwitch(entityId, hass)); }); // automatically activate the first speaker that's playing const firstPlayingSpeakerIndex = this.findFirstPlayingIndex(hass); this._selectedSpeaker = this.config.entities[firstPlayingSpeakerIndex]; this._tabs.setAttribute('selected', firstPlayingSpeakerIndex); return this._tabs; } buildStationList() { this._stationButtons = []; const stationList = document.createElement('div'); stationList.classList.add('station-list'); this.config.links.forEach(linkCfg => { const stationButton = this.buildStationSwitch(linkCfg.name, linkCfg.url) this._stationButtons.push(stationButton); stationList.appendChild(stationButton); }); // make sure the update method is notified of a change this._hassObservers.push(this.updateStationSwitchStates.bind(this)); return stationList; } buildVolumeSlider() { const volumeContainer = document.createElement('div'); volumeContainer.className = 'volume center horizontal layout'; const muteButton = document.createElement('paper-icon-button'); muteButton.icon = 'hass:volume-high'; muteButton.isMute = false; muteButton.addEventListener('click', this.onMuteUnmute.bind(this)); const slider = document.createElement('ha-paper-slider'); slider.min = 0; slider.max = 100; slider.addEventListener('change', this.onChangeVolumeSlider.bind(this)); slider.className = 'flex'; const stopButton = document.createElement('paper-icon-button') stopButton.icon = 'hass:stop'; stopButton.setAttribute('disabled', true); stopButton.addEventListener('click', this.onStop.bind(this)); this._hassObservers.push(hass => { if (!this._selectedSpeaker || !hass.states[this._selectedSpeaker]) { return; } const speakerState = hass.states[this._selectedSpeaker].attributes; // no speaker level? then hide mute button and volume if (!speakerState.hasOwnProperty('volume_level')) { slider.setAttribute('hidden', true); stopButton.setAttribute('hidden', true) } else { slider.removeAttribute('hidden'); stopButton.removeAttribute('hidden') } if (!speakerState.hasOwnProperty('is_volume_muted')) { muteButton.setAttribute('hidden', true); } else { muteButton.removeAttribute('hidden'); } if (hass.states[this._selectedSpeaker].state === 'playing') { stopButton.removeAttribute('disabled'); } else { stopButton.setAttribute('disabled', true); } slider.value = speakerState.volume_level ? speakerState.volume_level * 100 : 0; if (speakerState.is_volume_muted && !slider.disabled) { slider.disabled = true; muteButton.icon = 'hass:volume-off'; muteButton.isMute = true; } else if (!speakerState.is_volume_muted && slider.disabled) { slider.disabled = false; muteButton.icon = 'hass:volume-high'; muteButton.isMute = false; } }); volumeContainer.appendChild(muteButton); volumeContainer.appendChild(slider); volumeContainer.appendChild(stopButton); return volumeContainer; } onSpeakerSelect(entityId) { this._selectedSpeaker = entityId; this._hassObservers.forEach(listener => listener(this.hass)); } onChangeVolumeSlider(e) { const volPercentage = parseFloat(e.currentTarget.value); const vol = (volPercentage > 0 ? volPercentage / 100 : 0); this.setVolume(vol); } onMuteUnmute(e) { this.hass.callService('media_player', 'volume_mute', { entity_id: this._selectedSpeaker, is_volume_muted: !e.currentTarget.isMute }); } onStop(e) { this.hass.callService('media_player', 'media_stop', { entity_id: this._selectedSpeaker }); } updateStationSwitchStates(hass) { let playingUrl = null; const selectedSpeaker = this._selectedSpeaker; if (hass.states[selectedSpeaker] && hass.states[selectedSpeaker].state === 'playing') { playingUrl = hass.states[selectedSpeaker].attributes.media_content_id; } this._stationButtons.forEach(stationSwitch => { if (stationSwitch.hasAttribute('raised') && stationSwitch.stationUrl !== playingUrl) { stationSwitch.removeAttribute('raised'); return; } if (!stationSwitch.hasAttribute('raised') && stationSwitch.stationUrl === playingUrl) { stationSwitch.setAttribute('raised', true); } }) } buildStationSwitch(name, url) { const btn = document.createElement('paper-button'); btn.stationUrl = url; btn.className = 'juke-toggle'; btn.innerText = name; btn.addEventListener('click', this.onStationSelect.bind(this)); return btn; } onStationSelect(e) { this.hass.callService('media_player', 'play_media', { entity_id: this._selectedSpeaker, media_content_id: e.currentTarget.stationUrl, media_content_type: 'audio/mp4' }); } setVolume(value) { this.hass.callService('media_player', 'volume_set', { entity_id: this._selectedSpeaker, volume_level: value }); } /*** * returns the numeric index of the first entity in a "Playing" state, or 0 (first index). * * @param hass * @returns {number} * @private */ findFirstPlayingIndex(hass) { return Math.max(0, this.config.entities.findIndex(entityId => { return hass.states[entityId].state === 'playing'; })); } buildSpeakerSwitch(entityId, hass) { const entity = hass.states[entityId]; const btn = document.createElement('paper-tab'); btn.entityId = entityId; btn.innerText = hass.states[entityId].attributes.friendly_name; return btn; } setConfig(config) { if (!config.entities) { throw new Error('You need to define your media player entities'); } this.config = config; } getCardSize() { return 3; } } function getStyle() { const frag = document.createDocumentFragment(); const included = document.createElement('style'); included.setAttribute('include', 'iron-flex iron-flex-alignment'); const ownStyle = document.createElement('style'); ownStyle.innerHTML = ` .layout.horizontal, .layout.vertical { display: -ms-flexbox; display: -webkit-flex; display: flex; } .layout.horizontal { -ms-flex-direction: row; -webkit-flex-direction: row; flex-direction: row; } .layout.center, .layout.center-center { -ms-flex-align: center; -webkit-align-items: center; align-items: center; } .flex { ms-flex: 1 1 0.000000001px; -webkit-flex: 1; flex: 1; -webkit-flex-basis: 0.000000001px; flex-basis: 0.000000001px; } [hidden] { display: none !important; } .volume { padding: 10px 20px; } paper-button.juke-toggle[raised] { background-color: var(--primary-color); color: var(--text-primary-color); } paper-tabs { background-color: var(--primary-color); color: var(--text-primary-color); --paper-tabs-selection-bar-color: var(--text-primary-color, #FFF); } `; frag.appendChild(included); frag.appendChild(ownStyle); return frag; } customElements.define('jukebox-card', JukeboxCard);