diff --git a/gallery/src/demos/demo-hui-media-player-rows.js b/gallery/src/demos/demo-hui-media-player-rows.js
new file mode 100644
index 0000000000..04f3850721
--- /dev/null
+++ b/gallery/src/demos/demo-hui-media-player-rows.js
@@ -0,0 +1,105 @@
+import { html } from '@polymer/polymer/lib/utils/html-tag.js';
+import { PolymerElement } from '@polymer/polymer/polymer-element.js';
+
+import getEntity from '../data/entity.js';
+import provideHass from '../data/provide_hass.js';
+import '../components/demo-cards.js';
+
+const ENTITIES = [
+ getEntity('media_player', 'bedroom', 'playing', {
+ media_content_type: 'movie',
+ media_title: 'Epic sax guy 10 hours',
+ app_name: 'YouTube',
+ supported_features: 32
+ }),
+ getEntity('media_player', 'family_room', 'paused', {
+ media_content_type: 'music',
+ media_title: 'I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)',
+ media_artist: 'Technohead',
+ supported_features: 16417
+ }),
+ getEntity('media_player', 'family_room_no_play', 'paused', {
+ media_content_type: 'movie',
+ media_title: 'Epic sax guy 10 hours',
+ app_name: 'YouTube',
+ supported_features: 33
+ }),
+ getEntity('media_player', 'living_room', 'playing', {
+ media_content_type: 'tvshow',
+ media_title: 'Chapter 1',
+ media_series_title: 'House of Cards',
+ app_name: 'Netflix',
+ supported_features: 1
+ }),
+ getEntity('media_player', 'lounge_room', 'idle', {
+ media_content_type: 'music',
+ media_title: 'I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)',
+ media_artist: 'Technohead',
+ supported_features: 1,
+ }),
+ getEntity('media_player', 'theater', 'off', {
+ media_content_type: 'movie',
+ media_title: 'Epic sax guy 10 hours',
+ app_name: 'YouTube',
+ supported_features: 33
+ }),
+ getEntity('media_player', 'android_cast', 'playing', {
+ media_title: 'Android Screen Casting',
+ app_name: 'Screen Mirroring',
+ supported_features: 21437
+ }),
+];
+
+const CONFIGS = [
+ {
+ heading: 'Media Players',
+ config: `
+- type: entities
+ entities:
+ - entity: media_player.bedroom
+ name: Skip, no pause
+ - entity: media_player.family_room
+ name: Paused, music
+ - entity: media_player.family_room_no_play
+ name: Paused, no play
+ - entity: media_player.living_room
+ name: Pause, No skip, tvshow
+ - entity: media_player.android_cast
+ name: Screen casting
+ - entity: media_player.lounge_room
+ name: Chromcast Idle
+ - entity: media_player.theater
+ name: 'Player Off'
+ `
+ }
+];
+
+class DemoHuiMediaPlayerRows extends PolymerElement {
+ static get template() {
+ return html`
+
+ `;
+ }
+
+ static get properties() {
+ return {
+ _configs: {
+ type: Object,
+ value: CONFIGS
+ },
+ hass: Object,
+ };
+ }
+
+ ready() {
+ super.ready();
+ const hass = provideHass(this.$.demos);
+ hass.addEntities(ENTITIES);
+ }
+}
+
+customElements.define('demo-hui-media-player-rows', DemoHuiMediaPlayerRows);
diff --git a/src/panels/lovelace/common/create-row-element.js b/src/panels/lovelace/common/create-row-element.js
index 0cde4011d7..d4363295ca 100644
--- a/src/panels/lovelace/common/create-row-element.js
+++ b/src/panels/lovelace/common/create-row-element.js
@@ -7,6 +7,7 @@ import '../entity-rows/hui-input-number-entity-row.js';
import '../entity-rows/hui-input-select-entity-row.js';
import '../entity-rows/hui-input-text-entity-row.js';
import '../entity-rows/hui-lock-entity-row.js';
+import '../entity-rows/hui-media-player-entity-row.js';
import '../entity-rows/hui-scene-entity-row.js';
import '../entity-rows/hui-script-entity-row.js';
import '../entity-rows/hui-text-entity-row.js';
@@ -36,6 +37,7 @@ const DOMAIN_TO_ELEMENT_TYPE = {
input_select: 'input-select',
input_text: 'input-text',
light: 'toggle',
+ media_player: 'media-player',
lock: 'lock',
scene: 'scene',
script: 'script',
diff --git a/src/panels/lovelace/components/hui-generic-entity-row.js b/src/panels/lovelace/components/hui-generic-entity-row.js
index a2060148b0..1a15afaf8d 100644
--- a/src/panels/lovelace/components/hui-generic-entity-row.js
+++ b/src/panels/lovelace/components/hui-generic-entity-row.js
@@ -54,6 +54,9 @@ class HuiGenericEntityRow extends PolymerElement {
margin-left: 8px;
min-width: 0;
}
+ .flex ::slotted([slot=secondary]) {
+ margin-left: 0;
+ }
.secondary,
ha-relative-time {
display: block;
@@ -84,19 +87,22 @@ class HuiGenericEntityRow extends PolymerElement {
return html`
[[_computeName(config.name, _stateObj)]]
-
-
-
+
+
+
[[_stateObj.entity_id]]
-
+
+
+
+
-
-
+
+
-
+
`;
}
@@ -108,6 +114,10 @@ class HuiGenericEntityRow extends PolymerElement {
_stateObj: {
type: Object,
computed: '_computeStateObj(hass.states, config.entity)'
+ },
+ showSecondary: {
+ type: Boolean,
+ value: true
}
};
}
diff --git a/src/panels/lovelace/entity-rows/hui-media-player-entity-row.js b/src/panels/lovelace/entity-rows/hui-media-player-entity-row.js
new file mode 100644
index 0000000000..1c1a382cf8
--- /dev/null
+++ b/src/panels/lovelace/entity-rows/hui-media-player-entity-row.js
@@ -0,0 +1,142 @@
+import '@polymer/paper-icon-button/paper-icon-button.js';
+import { html } from '@polymer/polymer/lib/utils/html-tag.js';
+import { PolymerElement } from '@polymer/polymer/polymer-element.js';
+
+import '../components/hui-generic-entity-row.js';
+
+import LocalizeMixin from '../../../mixins/localize-mixin.js';
+
+const SUPPORT_PAUSE = 1;
+const SUPPORT_NEXT_TRACK = 32;
+const SUPPORTS_PLAY = 16384;
+const OFF_STATES = ['off', 'idle'];
+
+/*
+ * @appliesMixin LocalizeMixin
+ */
+class HuiMediaPlayerEntityRow extends LocalizeMixin(PolymerElement) {
+ static get template() {
+ return html`
+ ${this.styleTemplate}
+
+ ${this.mediaPlayerControlTemplate}
+
+ `;
+ }
+
+ static get styleTemplate() {
+ return html`
+
+ `;
+ }
+
+ static get mediaPlayerControlTemplate() {
+ return html`
+
+
+
+
+ [[_computeState(_stateObj.state)]]
+
+
+
+ [[_computeMediaTitle(_stateObj)]]
+
+ `;
+ }
+
+ static get properties() {
+ return {
+ hass: Object,
+ _config: Object,
+ _stateObj: {
+ type: Object,
+ computed: '_computeStateObj(hass.states, _config.entity)'
+ }
+ };
+ }
+
+ _computeStateObj(states, entityId) {
+ return states && entityId in states ? states[entityId] : null;
+ }
+
+ setConfig(config) {
+ if (!config || !config.entity) {
+ throw new Error('Entity not configured.');
+ }
+ this._config = config;
+ }
+
+ _computeControlIcon(stateObj) {
+ if (stateObj.state !== 'playing') {
+ return stateObj.attributes.supported_features & SUPPORTS_PLAY ? 'hass:play' : '';
+ }
+
+ return stateObj.attributes.supported_features & SUPPORT_PAUSE ? 'hass:pause' : 'hass:stop';
+ }
+
+ _computeMediaTitle(stateObj) {
+ if (!stateObj || this._isOff(stateObj.state)) return null;
+
+ switch (stateObj.attributes.media_content_type) {
+ case 'music':
+ return `${stateObj.attributes.media_artist}: ${stateObj.attributes.media_title}`;
+ case 'tvshow':
+ return `${stateObj.attributes.media_series_title}: ${stateObj.attributes.media_title}`;
+ default:
+ return stateObj.attributes.media_title || stateObj.attributes.app_name || stateObj.state;
+ }
+ }
+
+ _computeState(state) {
+ return this.localize(`state.media_player.${state}`)
+ || this.localize(`state.default.${state}`)
+ || state;
+ }
+
+ _callService(service) {
+ this.hass.callService('media_player', service, { entity_id: this._config.entity });
+ }
+
+ _playPause(event) {
+ event.stopPropagation();
+ this._callService('media_play_pause');
+ }
+
+ _nextTrack(event) {
+ event.stopPropagation();
+ if (this._stateObj.attributes.supported_features & SUPPORT_NEXT_TRACK) {
+ this._callService('media_next_track');
+ }
+ }
+
+ _isOff(state) {
+ return OFF_STATES.includes(state);
+ }
+
+ _supportsNext(stateObj) {
+ return stateObj && (stateObj.attributes.supported_features & SUPPORT_NEXT_TRACK);
+ }
+}
+customElements.define('hui-media-player-entity-row', HuiMediaPlayerEntityRow);