Custom UI: Mini media player

You have to set the icon in customize for the media player, there was a post about it but it’s disappeared for some.

Thanks. Most examples had it set under customize/input_text but this clears it up.

Is there an easy way to remove controls? For instance I have 3 sonos media players and would like to get rid of the album art and controls just leaving the volume slider for that player.

anybody know what I would use in hassio to play mp3’s when a service is executed (ie; turning on the light) using mopidy and a speaker hooked into the pi3 using a aux cable?

Would it be possible to use on/off instead of toggle for the power button?

It seems that Squeezebox players does not turn off when toggle.
Player starts when clicking first time but nothing happens when you try to turn it off (toggle called)

I added the icon configs to customize.yaml and for some reason ALL of my devices show an icon with cast. I have kodi and television on some others but they all still show as a cast icon. Anyone else have this happen?

I’m getting alot of these errors now:
2018-04-29 16:35:25 ERROR (MainThread) [frontend.js.latest.201804260] https://xxx.duckdns.org/frontend_latest/frontend-f9c85492ac8ddd09005dd3c9153053a5.html:1:7003 Uncaught TypeError: Cannot read property 'state' of null
Don’t think there is much difference if I change the javascript version.
Anything I can do to fix it?

HI,

After update to latest HA 0.68 I’m getting same error.
any luck resolving this issue ?

+1 Anyone found the issue?

Running 0.70.0 and I have the same issues :frowning:

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.