Custom UI: Mini media player

This is a quick fix. In this version I removed the dialog that pops up when tapping the mini player. This mini player needs to be rewritten to match up with Polymer 3. But until someone has the time to do this, this quickfix will work…

state-card-mini-media-player.html

<!--
https://github.com/c727/home-assistant-mini-media-player
version: 20180129.2
-->
<dom-module id='state-card-mini-media-player'>
  <template>
    <style include='iron-flex iron-flex-alignment'></style>
    <style>
      state-badge {
        float: left;
      }
      .info {
        margin-left: 56px;
      }
      .state,
      .playnername {
        line-height: 40px;
      }
      .playnername[has-info] {
        line-height: 20px;
      }
      .mediainfo {
        color: var(--secondary-text-color);
      }
      paper-dialog {
        padding: 20px 8px;
        border-radius: 2px;
        font-weight: 500;
        min-width: 500px;
      }
      @media screen and (max-width: 600px) {
        paper-dialog {
          position: absolute !important;
          bottom: 0; left: 0; right: 0;
          border-bottom-left-radius: 0;
          border-bottom-right-radius: 0;
          margin: 0;
        }
      }
      paper-button-group {
        display: grid;
        grid-template-columns: repeat(3, 120px);
        grid-gap: 4px;
        margin-top: 8px;
        padding: 0;
      }
      paper-button {
        background-color: var(--primary-color);
        color: #FFF;
        font-weight: 500;
        width: 100%;
        margin: 0;
      }
    </style>

    <template is='dom-if' if='[[!playerObj]]'>
      <div class='horizontal justified layout'>
        <state-info state-obj='[[stateObj]]' in-dialog='[[dialog]]'></state-info>
        <div class='state'>Offline</div>
      </div>
    </template>

    <template is='dom-if' if='[[playerObj]]'>
      <div class='horizontal justified layout'>
        <div class='state-info'>
          <state-badge state-obj='[[playerObj]]'></state-badge>
          <div class='info'>
            <div class='playnername' has-info$='[[playerObj.hasMediaInfo]]'>[[computeStateName(playerObj)]]</div>
            <div class='mediainfo' hidden='[[!playerObj.hasMediaInfo]]'>[[playerObj.mediaInfo]]</div>
          </div>
        </div>
        <div class='horizontal layout'>
          <paper-icon-button icon='mdi:power' on-tap='handlePowerTap'></paper-icon-button>
        </div>
      </div>

      <template is='dom-if' if='[[!playerObj.isOff]]'>
        <div class='info horizontal justified layout'>
          <paper-icon-button icon='mdi:volume-off' on-tap='handleVolumeTap' hidden='[[!playerObj.isMuted]]'></paper-icon-button>
          <paper-icon-button icon='mdi:volume-high' on-tap='handleVolumeTap' hidden='[[playerObj.isMuted]]'></paper-icon-button>
          <paper-slider disabled='[[playerObj.isMuted]]'
            min='0' max='100' value='[[playerObj.volumeSliderValue]]'
            on-change='volumeSliderChanged' class='flex'>
          </paper-slider>
          <div class='horizontal layout'>
            <paper-icon-button icon='mdi:skip-backward' on-tap='handlePreviousTap'></paper-icon-button>
            <paper-icon-button icon='mdi:play' on-tap='handlePlayPauseTap' hidden='[[playerObj.isPlaying]]'></paper-icon-button>
            <paper-icon-button icon='mdi:pause' on-tap='handlePlayPauseTap' hidden='[[!playerObj.isPlaying]]'></paper-icon-button>
            <paper-icon-button icon='mdi:skip-forward' on-tap='handleNextTap'></paper-icon-button>
          </div>
        </div>
      </template>
    </template>
  </template>
</dom-module>

<script>
class StateCardMiniMediaPlayer extends Polymer.Element {
  static get is() { return 'state-card-mini-media-player'; }
  
  static get properties() {
    return {
      hass: Object,
      stateObj: Object,
      config: {
        type: Object,
        computed: 'computeConfig(stateObj)',
      },
      playerObj: {
        type: Object,
        computed: 'computePlayerObj(hass, config)',
      },
      dialog: {
        type: Boolean,
        value: false,
      },
    };
  }

  computeConfig(stateObj) {     
    return stateObj.attributes.config;
  }

  computePlayerObj(hass, config) {
    if (config && config.player && hass.states[this.config.player]) {
      var playerObj = hass.states[this.config.player];
      playerObj.isOff = playerObj.state === 'off';
      playerObj.isPlaying = playerObj.state === 'playing';
      playerObj.hasMediaInfo = (playerObj.attributes.media_title || playerObj.attributes.app_name) ? true : false;
      if (playerObj.hasMediaInfo) {
        if (playerObj.attributes.media_title) {
          playerObj.mediaInfo = playerObj.attributes.media_artist ? playerObj.attributes.media_artist + ' - ' + playerObj.attributes.media_title : playerObj.attributes.media_title;
        } else {
          playerObj.mediaInfo = playerObj.attributes.app_name;
        }
      }
      playerObj.isMuted = playerObj.attributes.is_volume_muted;
      playerObj.volumeSliderValue = playerObj.attributes.volume_level * 100;
      return playerObj;
    }
    return null;
  }

  computeStateName(stateObj) {
    return stateObj.attributes && stateObj.attributes.friendly_name ? stateObj.attributes.friendly_name : stateObj.entity_id.substr(stateObj.entity_id.lastIndexOf('.')+1);
  }

  handlePreviousTap(ev) {
    ev.stopPropagation();
    this.callService('media_previous_track');
  }

  handlePlayPauseTap(ev) {
    ev.stopPropagation();
    this.callService('media_play_pause');
  }

  handleNextTap(ev) {
    ev.stopPropagation();
    this.callService('media_next_track');
  }

  handlePowerTap(ev) {
    ev.stopPropagation();
    this.callService('toggle');
  }

  callService(service) {
    this.hass.callService('media_player', service, { 'entity_id': this.playerObj.entity_id });
  }

  fireScript(ev) {      
    this.hass.callService('script', ev.model.item.script.split('.')[1], { 'entity_id': this.playerObj.entity_id });
  }

  handleVolumeTap() {
    this.hass.callService('media_player', 'volume_mute', { 'entity_id': this.playerObj.entity_id, 'is_volume_muted': !this.playerObj.isMuted });
  }
 
  volumeSliderChanged(ev) {
    var volPercentage = parseFloat(ev.target.value);
    var vol = volPercentage > 0 ? volPercentage / 100 : 0;
    this.hass.callService('media_player', 'volume_set', { 'entity_id': this.playerObj.entity_id, 'volume_level': vol });
  }

  
}
customElements.define(StateCardMiniMediaPlayer.is, StateCardMiniMediaPlayer);
</script>

state-card-mini-media-player_es5.html

<!--
https://github.com/c727/home-assistant-mini-media-player
version: 20180129.2
-->
<dom-module id='state-card-mini-media-player'>
  <template>
    <style include='iron-flex iron-flex-alignment'></style>
    <style>
      state-badge {
        float: left;
      }
      .info {
        margin-left: 56px;
      }
      .state,
      .playnername {
        line-height: 40px;
      }
      .playnername[has-info] {
        line-height: 20px;
      }
      .mediainfo {
        color: var(--secondary-text-color);
      }
      paper-dialog {
        padding: 20px 8px;
        border-radius: 2px;
        font-weight: 500;
        min-width: 500px;
      }
      @media screen and (max-width: 600px) {
        paper-dialog {
          position: absolute !important;
          bottom: 0; left: 0; right: 0;
          border-bottom-left-radius: 0;
          border-bottom-right-radius: 0;
          margin: 0;
        }
      }
      paper-button-group {
        display: grid;
        grid-template-columns: repeat(3, 120px);
        grid-gap: 4px;
        margin-top: 8px;
        padding: 0;
      }
      paper-button {
        background-color: var(--primary-color);
        color: #FFF;
        font-weight: 500;
        width: 100%;
        margin: 0;
      }
    </style>

    <template is='dom-if' if='[[!playerObj]]'>
      <div class='horizontal justified layout'>
        <state-info state-obj='[[stateObj]]' in-dialog='[[dialog]]'></state-info>
        <div class='state'>Offline</div>
      </div>
    </template>

    <template is='dom-if' if='[[playerObj]]'>
      <div class='horizontal justified layout'>
        <div class='state-info'>
          <state-badge state-obj='[[playerObj]]'></state-badge>
          <div class='info'>
            <div class='playnername' has-info$='[[playerObj.hasMediaInfo]]'>[[computeStateName(playerObj)]]</div>
            <div class='mediainfo' hidden='[[!playerObj.hasMediaInfo]]'>[[playerObj.mediaInfo]]</div>
          </div>
        </div>
        <div class='horizontal layout'>
          <paper-icon-button icon='mdi:power' on-tap='handlePowerTap'></paper-icon-button>
        </div>
      </div>

      <template is='dom-if' if='[[!playerObj.isOff]]'>
        <div class='info horizontal justified layout'>
          <paper-icon-button icon='mdi:volume-off' on-tap='handleVolumeTap' hidden='[[!playerObj.isMuted]]'></paper-icon-button>
          <paper-icon-button icon='mdi:volume-high' on-tap='handleVolumeTap' hidden='[[playerObj.isMuted]]'></paper-icon-button>
          <paper-slider disabled='[[playerObj.isMuted]]'
            min='0' max='100' value='[[playerObj.volumeSliderValue]]'
            on-change='volumeSliderChanged' class='flex'>
          </paper-slider>
          <div class='horizontal layout'>
            <paper-icon-button icon='mdi:skip-backward' on-tap='handlePreviousTap'></paper-icon-button>
            <paper-icon-button icon='mdi:play' on-tap='handlePlayPauseTap' hidden='[[playerObj.isPlaying]]'></paper-icon-button>
            <paper-icon-button icon='mdi:pause' on-tap='handlePlayPauseTap' hidden='[[!playerObj.isPlaying]]'></paper-icon-button>
            <paper-icon-button icon='mdi:skip-forward' on-tap='handleNextTap'></paper-icon-button>
          </div>
        </div>
      </template>
    </template>
  </template>
</dom-module>

<script>
  Polymer({
    is: 'state-card-mini-media-player',
    properties: {
      hass: {
        type: Object,
      },
      stateObj: {
        type: Object,
      },
      config: {
        type: Object,
        computed: 'computeConfig(stateObj)',
      },
      playerObj: {
        type: Object,
        computed: 'computePlayerObj(hass, config)',
      },
      dialog: {
        type: Boolean,
        value: false,
      },
    },

    computeConfig: function (stateObj) {     
      return stateObj.attributes.config;
    },

    computePlayerObj: function (hass, config) {     
      if (config && config.player && hass.states[this.config.player]) {
        var playerObj = hass.states[this.config.player];
        playerObj.isOff = playerObj.state === 'off';
        playerObj.isPlaying = playerObj.state === 'playing';
        playerObj.hasMediaInfo = (playerObj.attributes.media_title || playerObj.attributes.app_name) ? true : false;
        if (playerObj.hasMediaInfo) {
          if (playerObj.attributes.media_title) {
            playerObj.mediaInfo = playerObj.attributes.media_artist ? playerObj.attributes.media_artist + ' - ' + playerObj.attributes.media_title : playerObj.attributes.media_title;
          } else {
            playerObj.mediaInfo = playerObj.attributes.app_name;
          }
        }
        playerObj.isMuted = playerObj.attributes.is_volume_muted;
        playerObj.volumeSliderValue = playerObj.attributes.volume_level * 100;
        return playerObj;
      }
      return null;
    },

    computeStateName: function (stateObj) {     
      return stateObj.attributes && stateObj.attributes.friendly_name ? stateObj.attributes.friendly_name : stateObj.entity_id.substr(stateObj.entity_id.lastIndexOf('.')+1);
    },

    handlePreviousTap: function (ev) {     
      ev.stopPropagation();
      this.callService('media_previous_track');
    },

    handlePlayPauseTap: function (ev) {     
      ev.stopPropagation();
      this.callService('media_play_pause');
    },

    handleNextTap: function (ev) {     
      ev.stopPropagation();
      this.callService('media_next_track');
    },

    handlePowerTap: function (ev) {     
      ev.stopPropagation();
      this.callService('toggle');
    },

    callService: function (service) {     
      this.hass.callService('media_player', service, { 'entity_id': this.playerObj.entity_id });
    },

    fireScript: function (ev) {     
      this.hass.callService('script', ev.model.item.script.split('.')[1], { 'entity_id': this.playerObj.entity_id });
    },

    handleVolumeTap: function () {     
      this.hass.callService('media_player', 'volume_mute', { 'entity_id': this.playerObj.entity_id, 'is_volume_muted': !this.playerObj.isMuted });
    },

    volumeSliderChanged: function (ev) {     
      var volPercentage = parseFloat(ev.target.value);
      var vol = volPercentage > 0 ? volPercentage / 100 : 0;
      this.hass.callService('media_player', 'volume_set', { 'entity_id': this.playerObj.entity_id, 'volume_level': vol });
    },

  });
</script>
4 Likes

This is excellent and i have been using and enjoying it for many months now so thank you very much.
I am now trying Lovelace UI, has anyone been able to get this card to work with it? I know in examples it supports the standard player but after getting used to this i dont want to go back!

Thanks

Lovelace is only for playing with at the moment, like it says in the release.

1 Like

yep fully appreciate that. but the last thing stopping me from switching completely is i cannot work out from the documentation how to reference the existing html files for this.

Really liking new glance view for entities just wish the default media player had same attention. (Absolutely no need for the very large cover art square, as requires click to get to the majority of controls. This mini media player will fit in nicely!)

I really need to start digging into this Lovelace stuff, looks great. Let us know if you make any progress

I played around a little with Lovelace a week ago. Due to the nature of how Lovelace is designed, the card is not contained in an ha-entities-card and so does look quite ugly which is why I stopped working on it for now until maybe Lovelace supports custom-ui from config or I feel the need to work around it.

ha

can you share what you changed to get that far?
To be honest that is all i want from the media player, but my changes have not worked i either get “cannot be found” or “offline” but no actual controller.

Many thanks

I seem to have two volume sliders for each media player. Is this correct or have I messed something up?

recently i noticed my mini players not showing up in the HTML / laptop view. On the HASS app on Android it did work.

Only adding this line fixed it for me.

frontend:
javascript_version: es5

Any downside on forcing es5 ?

Could have something to do with a change 0.71, but I also see the author made a fix. However, for me, it does not seem to work as earlier. I cannot see my buttons nor choose source.

Forget what I wrote about the buttons and source. Was a cache issue. However, I am also now experiencing the same as @getsmokes

Hmm, no luck. I think I’ll remove it from my front end for now.

I’m also getting 2 volume sliders and power buttons. Anyone have any ideas?

is some one looking into it? I tried but I’m lost with all that polymer stuff

Can anyone help me out at all. I must be missing something silly, but I can’t seem to change the icon’s on the mini player. They are all coming up with Cast icon’s no matter what I set.

input_text.dummy_player_kitchen:
  icon: mdi:kettle
  custom_ui_state_card: state-card-mini-media-player
  config:
    player: media_player.kitchen_speaker
input_text.dummy_player_bedroom:
  icon: mdi:cast
  custom_ui_state_card: state-card-mini-media-player
  config:
    player: media_player.bedroom_speaker
input_text.dummy_player_living_room:
  icon: mdi:cast
  custom_ui_state_card: state-card-mini-media-player
  config:
    player: media_player.living_room_speaker
input_text.dummy_player_garage:
  icon: mdi:cast
  custom_ui_state_card: state-card-mini-media-player
  config:
    player: media_player.garage_speaker
input_text.dummy_player_bathroom:
  icon: mdi:cast
  custom_ui_state_card: state-card-mini-media-player
  config:
    player: media_player.bathroom_speaker
input_text.dummy_player_pioneer_amp:
  icon: mdi-music-box-outline
  custom_ui_state_card: state-card-mini-media-player
  config:
    player: media_player.pioneer_amp
input_text.dummy_player_mpd:
  icon: mdi:play-network
  custom_ui_state_card: state-card-mini-media-player
  config:
    player: media_player.mpd
input_text.dummy_player_livingroom_tv:
  icon: mdi:television-classic
  custom_ui_state_card: state-card-mini-media-player
  config:
    player: media_player.living_room_tv
input_text.dummy_player_kodiroom:
  icon: mdi-kodi
  custom_ui_state_card: state-card-mini-media-player
  config:
    player: media_player.kodi_living_room

As you can see I have tried everything, mdi: mdi- community, and non community icon’s, but it only ever loads the cast icon. Even if I remove the icon, it still shows up as the cast icon.

I am using chrome, have run it without cache loaded, tried it in IE as well and on the phone, still the same.

Is there something hard coded in the .html ?

I am using @Anders_Karlstrom code from post 81.

Apologise in advance if I missed something really dumb

customize:
  input_text.dummy_player_kitchen:
    icon: mdi:kettle 

input_text.dummy_player_kitchen:
  custom_ui_state_card: state-card-mini-media-player
  config:
    player: media_player.kitchen_speaker

etc

1 Like

Thank you for the reply @anon43302295 I should have said, that was a cut of my code from customize.yaml.

the custom state card has to be added in the customize section I believe, I tried adding to the config on the input text, but it just throws an error up.

I tried to split it in separate lines, not sure why, but that again (obviously) threw up an error.

Tried moving the player code to customize_glob.yaml, but thats for the other custom card, and left the icon code in the customize.yaml, and it changed the icons, but then obviously the players doesn’t work :laughing:

So still stuck as to why it wont change them. I can see that @Cezex managed to do it above, its his code I am using, so not sure why it doesn’t work :frowning:

Yeah sorry, I mistyped it trying to remember it on the train, I meant:

customize:
  media_player.kitchen_speaker:  #this line is the important one 
    icon: mdi:kettle 

  input_text.dummy_player_kitchen:
    custom_ui_state_card: state-card-mini-media-player
    config:
      player: media_player.kitchen_speaker
1 Like

Now that makes a lot more sense, why didn’t I think of that. Let me give it a try.

worked perfectly, thank you very much @anon43302295 :+1:

1 Like