Merge pull request #158 from balloob/refactor-media-player

Initial refactor media player
This commit is contained in:
Paulus Schoutsen 2015-06-10 23:59:53 -07:00
commit 2ea195a5d2
14 changed files with 1073 additions and 407 deletions

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "1cc966bcef26a859d053bd5c46769a99"
VERSION = "d0b5982b1bc41a96b8a4f49aaa92af5d"

View File

@ -16984,7 +16984,7 @@ window.hass.uiUtil.domainIcon = function(domain, state) {
case "media_player":
var icon = "hardware:cast";
if (state && state !== "idle") {
if (state && state !== "off" && state !== 'idle') {
icon += "-connected";
}
@ -22312,8 +22312,8 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
<div class="horizontal justified layout">
<state-info state-obj="[[stateObj]]"></state-info>
<div class="state">
<div class="main-text">[[computePrimaryText(stateObj)]]</div>
<div class="secondary-text">[[computeSecondaryText(stateObj)]]</div>
<div class="main-text">[[computePrimaryText(stateObj, isPlaying)]]</div>
<div class="secondary-text">[[computeSecondaryText(stateObj, isPlaying)]]</div>
</div>
</div>
</template>
@ -22321,6 +22321,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
<script>
(function() {
var PLAYING_STATES = ['playing', 'paused'];
Polymer({
is: 'state-card-media_player',
@ -22328,14 +22329,41 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
stateObj: {
type: Object,
},
isPlaying: {
type: Boolean,
computed: 'computeIsPlaying(stateObj)',
},
},
computePrimaryText: function(stateObj) {
return stateObj.attributes.media_title || stateObj.stateDisplay;
computeIsPlaying: function(stateObj) {
return PLAYING_STATES.indexOf(stateObj.state) !== -1;
},
computeSecondaryText: function(stateObj) {
return stateObj.attributes.media_title ? stateObj.stateDisplay : '';
computePrimaryText: function(stateObj, isPlaying) {
return isPlaying ? stateObj.attributes.media_title : stateObj.stateDisplay;
},
computeSecondaryText: function(stateObj, isPlaying) {
var text;
if (stateObj.attributes.media_content_type == 'music') {
return stateObj.attributes.media_artist;
} else if (stateObj.attributes.media_content_type == 'tvshow') {
text = stateObj.attributes.media_series_title;
if (stateObj.attributes.media_season && stateObj.attributes.media_episode) {
text += ' S' + stateObj.attributes.media_season + 'E' + stateObj.attributes.media_episode;
}
return text;
} else if (stateObj.attributes.app_name) {
return stateObj.attributes.app_name;
} else {
return '';
}
},
});
})();
@ -22353,6 +22381,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
width: 100%;
cursor: pointer;
overflow: hidden;
}
</style>
@ -25819,8 +25848,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
text-transform: capitalize;
}
/* Accent the power button because the user should use that first */
paper-icon-button[focus] {
paper-icon-button[highlight] {
color: var(--accent-color);
}
@ -25832,7 +25860,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
transition: max-height .5s ease-in;
}
.has-media_volume .volume {
.has-volume_level .volume {
max-height: 40px;
}
</style>
@ -25840,19 +25868,19 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
<div class$="[[computeClassNames(stateObj)]]">
<div class="layout horizontal">
<div class="flex">
<paper-icon-button icon="power-settings-new" focus$="[[isIdle]]" on-tap="handleTogglePower"></paper-icon-button>
<paper-icon-button icon="power-settings-new" highlight$="[[isOff]]" on-tap="handleTogglePower" hidden$="[[computeHidePowerButton(isOff, supportsTurnOn, supportsTurnOff)]]"></paper-icon-button>
</div>
<div>
<template is="dom-if" if="[[!isIdle]]">
<paper-icon-button icon="av:skip-previous" on-tap="handlePrevious"></paper-icon-button>
<paper-icon-button icon="[[computePlayPauseIcon(stateObj)]]" focus$="" on-tap="handlePlayPause"></paper-icon-button>
<paper-icon-button icon="av:skip-next" on-tap="handleNext"></paper-icon-button>
<template is="dom-if" if="[[!isOff]]">
<paper-icon-button icon="av:skip-previous" on-tap="handlePrevious" hidden$="[[!supportsPreviousTrack]]"></paper-icon-button>
<paper-icon-button icon="[[computePlaybackControlIcon(stateObj)]]" on-tap="handlePlaybackControl" highlight=""></paper-icon-button>
<paper-icon-button icon="av:skip-next" on-tap="handleNext" hidden$="[[!supportsNextTrack]]"></paper-icon-button>
</template>
</div>
</div>
<div class="volume center horizontal layout">
<div class="volume center horizontal layout" hidden$="[[!supportsVolumeSet]]">
<paper-icon-button on-tap="handleVolumeTap" icon="[[computeMuteVolumeIcon(isMuted)]]"></paper-icon-button>
<paper-slider hidden="[[isMuted]]" min="0" max="100" value="{{volumeSliderValue}}" on-change="volumeSliderChanged" class="flex">
<paper-slider disabled$="[[isMuted]]" min="0" max="100" value="[[volumeSliderValue]]" on-change="volumeSliderChanged" class="flex">
</paper-slider>
</div>
</div>
@ -25863,7 +25891,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
(function() {
var serviceActions = window.hass.serviceActions;
var uiUtil = window.hass.uiUtil;
var ATTRIBUTE_CLASSES = ['media_volume'];
var ATTRIBUTE_CLASSES = ['volume_level'];
Polymer({
is: 'more-info-media_player',
@ -25874,9 +25902,14 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
observer: 'stateObjChanged',
},
isIdle: {
isOff: {
type: Boolean,
computed: 'computeIsIdle(stateObj)',
value: false,
},
isPlaying: {
type: Boolean,
value: false,
},
isMuted: {
@ -25887,13 +25920,58 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
volumeSliderValue: {
type: Number,
value: 0,
}
},
supportsPause: {
type: Boolean,
value: false,
},
supportsVolumeSet: {
type: Boolean,
value: false,
},
supportsVolumeMute: {
type: Boolean,
value: false,
},
supportsPreviousTrack: {
type: Boolean,
value: false,
},
supportsNextTrack: {
type: Boolean,
value: false,
},
supportsTurnOn: {
type: Boolean,
value: false,
},
supportsTurnOff: {
type: Boolean,
value: false,
},
},
stateObjChanged: function(newVal, oldVal) {
if (newVal) {
this.volumeSliderValue = newVal.attributes.media_volume * 100;
this.isMuted = newVal.attributes.media_is_volume_muted;
this.isOff = newVal.state == 'off';
this.isPlaying = newVal.state == 'playing';
this.volumeSliderValue = newVal.attributes.volume_level * 100;
this.isMuted = newVal.attributes.is_volume_muted;
this.supportsPause = (newVal.attributes.supported_media_commands & 1) !== 0;
this.supportsVolumeSet = (newVal.attributes.supported_media_commands & 4) !== 0;
this.supportsVolumeMute = (newVal.attributes.supported_media_commands & 8) !== 0;
this.supportsPreviousTrack = (newVal.attributes.supported_media_commands & 16) !== 0;
this.supportsNextTrack = (newVal.attributes.supported_media_commands & 32) !== 0;
this.supportsTurnOn = (newVal.attributes.supported_media_commands & 128) !== 0;
this.supportsTurnOff = (newVal.attributes.supported_media_commands & 256) !== 0;
}
this.debounce('more-info-volume-animation-finish', function() {
@ -25905,35 +25983,37 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
return uiUtil.attributeClassNames(stateObj, ATTRIBUTE_CLASSES);
},
computeMediaState: function(stateObj) {
return stateObj.state == 'idle' ? 'idle' : stateObj.attributes.media_state;
},
computeIsIdle: function(stateObj) {
return stateObj.state == 'idle';
},
computePowerButtonCaption: function(isIdle) {
return isIdle ? 'Turn on' : 'Turn off';
computeIsOff: function(stateObj) {
return stateObj.state == 'off';
},
computeMuteVolumeIcon: function(isMuted) {
return isMuted ? 'av:volume-off' : 'av:volume-up';
},
computePlayPauseIcon: function(stateObj) {
return stateObj.attributes.media_state == 'playing' ? 'av:pause' : 'av:play-arrow';
computePlaybackControlIcon: function(stateObj) {
if (this.isPlaying) {
return this.supportsPause ? 'av:pause' : 'av:stop';
}
return 'av:play-arrow';
},
computeHidePowerButton: function(isOff, supportsTurnOn, supportsTurnOff) {
return isOff && !supportsTurnOn || !isOff && supportsTurnOff;
},
handleTogglePower: function() {
this.callService(this.isIdle ? 'turn_on' : 'turn_off');
this.callService(this.isOff ? 'turn_on' : 'turn_off');
},
handlePrevious: function() {
this.callService('media_prev_track');
this.callService('media_previous_track');
},
handlePlayPause: function() {
handlePlaybackControl: function() {
if (this.isPlaying && !this.supportsPause) {
alert('This case is not supported yet');
}
this.callService('media_play_pause');
},
@ -25942,14 +26022,16 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
},
handleVolumeTap: function() {
this.callService('volume_mute', { mute: !this.isMuted });
if (!this.supportsVolumeMute) {
return;
}
this.callService('volume_mute', { is_volume_muted: !this.isMuted });
},
volumeSliderChanged: function(ev) {
var volPercentage = parseFloat(ev.target.value);
var vol = volPercentage > 0 ? volPercentage / 100 : 0;
this.callService('volume_set', { volume: vol });
this.callService('volume_set', { volume_level: vol });
},
callService: function(service, data) {

View File

@ -33,8 +33,8 @@
<div class='horizontal justified layout'>
<state-info state-obj="[[stateObj]]"></state-info>
<div class='state'>
<div class='main-text'>[[computePrimaryText(stateObj)]]</div>
<div class='secondary-text'>[[computeSecondaryText(stateObj)]]</div>
<div class='main-text'>[[computePrimaryText(stateObj, isPlaying)]]</div>
<div class='secondary-text'>[[computeSecondaryText(stateObj, isPlaying)]]</div>
</div>
</div>
</template>
@ -42,6 +42,7 @@
<script>
(function() {
var PLAYING_STATES = ['playing', 'paused'];
Polymer({
is: 'state-card-media_player',
@ -49,14 +50,41 @@
stateObj: {
type: Object,
},
isPlaying: {
type: Boolean,
computed: 'computeIsPlaying(stateObj)',
},
},
computePrimaryText: function(stateObj) {
return stateObj.attributes.media_title || stateObj.stateDisplay;
computeIsPlaying: function(stateObj) {
return PLAYING_STATES.indexOf(stateObj.state) !== -1;
},
computeSecondaryText: function(stateObj) {
return stateObj.attributes.media_title ? stateObj.stateDisplay : '';
computePrimaryText: function(stateObj, isPlaying) {
return isPlaying ? stateObj.attributes.media_title : stateObj.stateDisplay;
},
computeSecondaryText: function(stateObj, isPlaying) {
var text;
if (stateObj.attributes.media_content_type == 'music') {
return stateObj.attributes.media_artist;
} else if (stateObj.attributes.media_content_type == 'tvshow') {
text = stateObj.attributes.media_series_title;
if (stateObj.attributes.media_season && stateObj.attributes.media_episode) {
text += ' S' + stateObj.attributes.media_season + 'E' + stateObj.attributes.media_episode;
}
return text;
} else if (stateObj.attributes.app_name) {
return stateObj.attributes.app_name;
} else {
return '';
}
},
});
})();

View File

@ -15,6 +15,7 @@
width: 100%;
cursor: pointer;
overflow: hidden;
}
</style>

View File

@ -8,8 +8,7 @@
text-transform: capitalize;
}
/* Accent the power button because the user should use that first */
paper-icon-button[focus] {
paper-icon-button[highlight] {
color: var(--accent-color);
}
@ -21,7 +20,7 @@
transition: max-height .5s ease-in;
}
.has-media_volume .volume {
.has-volume_level .volume {
max-height: 40px;
}
</style>
@ -29,25 +28,26 @@
<div class$='[[computeClassNames(stateObj)]]'>
<div class='layout horizontal'>
<div class='flex'>
<paper-icon-button icon='power-settings-new' focus$='[[isIdle]]'
on-tap='handleTogglePower'></paper-icon-button>
<paper-icon-button icon='power-settings-new' highlight$='[[isOff]]'
on-tap='handleTogglePower'
hidden$='[[computeHidePowerButton(isOff, supportsTurnOn, supportsTurnOff)]]'></paper-icon-button>
</div>
<div>
<template is='dom-if' if='[[!isIdle]]'>
<paper-icon-button icon='av:skip-previous'
on-tap='handlePrevious'></paper-icon-button>
<paper-icon-button icon='[[computePlayPauseIcon(stateObj)]]' focus$
on-tap='handlePlayPause'></paper-icon-button>
<paper-icon-button icon='av:skip-next'
on-tap='handleNext'></paper-icon-button>
<template is='dom-if' if='[[!isOff]]'>
<paper-icon-button icon='av:skip-previous' on-tap='handlePrevious'
hidden$='[[!supportsPreviousTrack]]'></paper-icon-button>
<paper-icon-button icon='[[computePlaybackControlIcon(stateObj)]]'
on-tap='handlePlaybackControl' highlight></paper-icon-button>
<paper-icon-button icon='av:skip-next' on-tap='handleNext'
hidden$='[[!supportsNextTrack]]'></paper-icon-button>
</template>
</div>
</div>
<div class='volume center horizontal layout'>
<div class='volume center horizontal layout' hidden$='[[!supportsVolumeSet]]'>
<paper-icon-button on-tap="handleVolumeTap"
icon="[[computeMuteVolumeIcon(isMuted)]]"></paper-icon-button>
<paper-slider hidden='[[isMuted]]'
min='0' max='100' value='{{volumeSliderValue}}'
<paper-slider disabled$='[[isMuted]]'
min='0' max='100' value='[[volumeSliderValue]]'
on-change='volumeSliderChanged' class='flex'>
</paper-slider>
</div>
@ -59,7 +59,7 @@
(function() {
var serviceActions = window.hass.serviceActions;
var uiUtil = window.hass.uiUtil;
var ATTRIBUTE_CLASSES = ['media_volume'];
var ATTRIBUTE_CLASSES = ['volume_level'];
Polymer({
is: 'more-info-media_player',
@ -70,9 +70,14 @@
observer: 'stateObjChanged',
},
isIdle: {
isOff: {
type: Boolean,
computed: 'computeIsIdle(stateObj)',
value: false,
},
isPlaying: {
type: Boolean,
value: false,
},
isMuted: {
@ -83,13 +88,58 @@
volumeSliderValue: {
type: Number,
value: 0,
}
},
supportsPause: {
type: Boolean,
value: false,
},
supportsVolumeSet: {
type: Boolean,
value: false,
},
supportsVolumeMute: {
type: Boolean,
value: false,
},
supportsPreviousTrack: {
type: Boolean,
value: false,
},
supportsNextTrack: {
type: Boolean,
value: false,
},
supportsTurnOn: {
type: Boolean,
value: false,
},
supportsTurnOff: {
type: Boolean,
value: false,
},
},
stateObjChanged: function(newVal, oldVal) {
if (newVal) {
this.volumeSliderValue = newVal.attributes.media_volume * 100;
this.isMuted = newVal.attributes.media_is_volume_muted;
this.isOff = newVal.state == 'off';
this.isPlaying = newVal.state == 'playing';
this.volumeSliderValue = newVal.attributes.volume_level * 100;
this.isMuted = newVal.attributes.is_volume_muted;
this.supportsPause = (newVal.attributes.supported_media_commands & 1) !== 0;
this.supportsVolumeSet = (newVal.attributes.supported_media_commands & 4) !== 0;
this.supportsVolumeMute = (newVal.attributes.supported_media_commands & 8) !== 0;
this.supportsPreviousTrack = (newVal.attributes.supported_media_commands & 16) !== 0;
this.supportsNextTrack = (newVal.attributes.supported_media_commands & 32) !== 0;
this.supportsTurnOn = (newVal.attributes.supported_media_commands & 128) !== 0;
this.supportsTurnOff = (newVal.attributes.supported_media_commands & 256) !== 0;
}
this.debounce('more-info-volume-animation-finish', function() {
@ -101,35 +151,37 @@
return uiUtil.attributeClassNames(stateObj, ATTRIBUTE_CLASSES);
},
computeMediaState: function(stateObj) {
return stateObj.state == 'idle' ? 'idle' : stateObj.attributes.media_state;
},
computeIsIdle: function(stateObj) {
return stateObj.state == 'idle';
},
computePowerButtonCaption: function(isIdle) {
return isIdle ? 'Turn on' : 'Turn off';
computeIsOff: function(stateObj) {
return stateObj.state == 'off';
},
computeMuteVolumeIcon: function(isMuted) {
return isMuted ? 'av:volume-off' : 'av:volume-up';
},
computePlayPauseIcon: function(stateObj) {
return stateObj.attributes.media_state == 'playing' ? 'av:pause' : 'av:play-arrow';
computePlaybackControlIcon: function(stateObj) {
if (this.isPlaying) {
return this.supportsPause ? 'av:pause' : 'av:stop';
}
return 'av:play-arrow';
},
computeHidePowerButton: function(isOff, supportsTurnOn, supportsTurnOff) {
return isOff && !supportsTurnOn || !isOff && supportsTurnOff;
},
handleTogglePower: function() {
this.callService(this.isIdle ? 'turn_on' : 'turn_off');
this.callService(this.isOff ? 'turn_on' : 'turn_off');
},
handlePrevious: function() {
this.callService('media_prev_track');
this.callService('media_previous_track');
},
handlePlayPause: function() {
handlePlaybackControl: function() {
if (this.isPlaying && !this.supportsPause) {
alert('This case is not supported yet');
}
this.callService('media_play_pause');
},
@ -138,14 +190,16 @@
},
handleVolumeTap: function() {
this.callService('volume_mute', { mute: !this.isMuted });
if (!this.supportsVolumeMute) {
return;
}
this.callService('volume_mute', { is_volume_muted: !this.isMuted });
},
volumeSliderChanged: function(ev) {
var volPercentage = parseFloat(ev.target.value);
var vol = volPercentage > 0 ? volPercentage / 100 : 0;
this.callService('volume_set', { volume: vol });
this.callService('volume_set', { volume_level: vol });
},
callService: function(service, data) {

View File

@ -48,7 +48,7 @@ window.hass.uiUtil.domainIcon = function(domain, state) {
case "media_player":
var icon = "hardware:cast";
if (state && state !== "idle") {
if (state && state !== "off" && state !== 'idle') {
icon += "-connected";
}

View File

@ -8,7 +8,7 @@ import logging
from homeassistant.const import (
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_MEDIA_PLAY_PAUSE)
@ -43,7 +43,7 @@ def media_next_track(hass):
def media_prev_track(hass):
""" Press the keyboard button for prev track. """
hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK)
hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK)
def setup(hass, config):
@ -79,7 +79,7 @@ def setup(hass, config):
lambda service:
keyboard.tap_key(keyboard.media_next_track_key))
hass.services.register(DOMAIN, SERVICE_MEDIA_PREV_TRACK,
hass.services.register(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK,
lambda service:
keyboard.tap_key(keyboard.media_prev_track_key))

View File

@ -10,11 +10,12 @@ from homeassistant.components import discovery
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON,
STATE_OFF, STATE_UNKNOWN, STATE_PLAYING,
ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET,
SERVICE_VOLUME_MUTE,
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK)
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK)
DOMAIN = 'media_player'
DEPENDENCIES = []
@ -28,28 +29,70 @@ DISCOVERY_PLATFORMS = {
SERVICE_YOUTUBE_VIDEO = 'play_youtube_video'
STATE_NO_APP = 'idle'
ATTR_STATE = 'state'
ATTR_OPTIONS = 'options'
ATTR_MEDIA_STATE = 'media_state'
ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted'
ATTR_MEDIA_SEEK_POSITION = 'seek_position'
ATTR_MEDIA_CONTENT_ID = 'media_content_id'
ATTR_MEDIA_CONTENT_TYPE = 'media_content_type'
ATTR_MEDIA_DURATION = 'media_duration'
ATTR_MEDIA_TITLE = 'media_title'
ATTR_MEDIA_ARTIST = 'media_artist'
ATTR_MEDIA_ALBUM = 'media_album'
ATTR_MEDIA_IMAGE_URL = 'media_image_url'
ATTR_MEDIA_VOLUME = 'media_volume'
ATTR_MEDIA_IS_VOLUME_MUTED = 'media_is_volume_muted'
ATTR_MEDIA_DURATION = 'media_duration'
ATTR_MEDIA_DATE = 'media_date'
ATTR_MEDIA_ALBUM_NAME = 'media_album_name'
ATTR_MEDIA_ALBUM_ARTIST = 'media_album_artist'
ATTR_MEDIA_TRACK = 'media_track'
ATTR_MEDIA_SERIES_TITLE = 'media_series_title'
ATTR_MEDIA_SEASON = 'media_season'
ATTR_MEDIA_EPISODE = 'media_episode'
ATTR_APP_ID = 'app_id'
ATTR_APP_NAME = 'app_name'
ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands'
MEDIA_STATE_UNKNOWN = 'unknown'
MEDIA_STATE_PLAYING = 'playing'
MEDIA_STATE_PAUSED = 'paused'
MEDIA_STATE_STOPPED = 'stopped'
MEDIA_TYPE_MUSIC = 'music'
MEDIA_TYPE_TVSHOW = 'tvshow'
MEDIA_TYPE_VIDEO = 'movie'
SUPPORT_PAUSE = 1
SUPPORT_SEEK = 2
SUPPORT_VOLUME_SET = 4
SUPPORT_VOLUME_MUTE = 8
SUPPORT_PREVIOUS_TRACK = 16
SUPPORT_NEXT_TRACK = 32
SUPPORT_YOUTUBE = 64
SUPPORT_TURN_ON = 128
SUPPORT_TURN_OFF = 256
YOUTUBE_COVER_URL_FORMAT = 'http://img.youtube.com/vi/{}/1.jpg'
YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg'
SERVICE_TO_METHOD = {
SERVICE_TURN_ON: 'turn_on',
SERVICE_TURN_OFF: 'turn_off',
SERVICE_VOLUME_UP: 'volume_up',
SERVICE_VOLUME_DOWN: 'volume_down',
SERVICE_MEDIA_PLAY_PAUSE: 'media_play_pause',
SERVICE_MEDIA_PLAY: 'media_play',
SERVICE_MEDIA_PAUSE: 'media_pause',
SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track',
}
ATTR_TO_PROPERTY = [
ATTR_MEDIA_VOLUME_LEVEL,
ATTR_MEDIA_VOLUME_MUTED,
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
ATTR_MEDIA_DURATION,
ATTR_MEDIA_TITLE,
ATTR_MEDIA_ARTIST,
ATTR_MEDIA_ALBUM_NAME,
ATTR_MEDIA_ALBUM_ARTIST,
ATTR_MEDIA_TRACK,
ATTR_MEDIA_SERIES_TITLE,
ATTR_MEDIA_SEASON,
ATTR_MEDIA_EPISODE,
ATTR_APP_ID,
ATTR_APP_NAME,
ATTR_SUPPORTED_MEDIA_COMMANDS,
]
def is_on(hass, entity_id=None):
@ -58,7 +101,7 @@ def is_on(hass, entity_id=None):
entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN)
return any(not hass.states.is_state(entity_id, STATE_NO_APP)
return any(not hass.states.is_state(entity_id, STATE_OFF)
for entity_id in entity_ids)
@ -90,21 +133,22 @@ def volume_down(hass, entity_id=None):
hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data)
def volume_mute(hass, entity_id=None):
""" Send the media player the command to toggle its mute state. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
def mute_volume(hass, mute, entity_id=None):
""" Send the media player the command for volume down. """
data = {ATTR_MEDIA_VOLUME_MUTED: mute}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, data)
def volume_set(hass, entity_id=None, volume=None):
""" Set volume on media player. """
data = {
key: value for key, value in [
(ATTR_ENTITY_ID, entity_id),
(ATTR_MEDIA_VOLUME, volume),
] if value is not None
}
def set_volume_level(hass, volume, entity_id=None):
""" Send the media player the command for volume down. """
data = {ATTR_MEDIA_VOLUME_LEVEL: volume}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_VOLUME_SET, data)
@ -137,24 +181,11 @@ def media_next_track(hass, entity_id=None):
hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data)
def media_prev_track(hass, entity_id=None):
def media_previous_track(hass, entity_id=None):
""" Send the media player the command for prev track. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK, data)
SERVICE_TO_METHOD = {
SERVICE_TURN_ON: 'turn_on',
SERVICE_TURN_OFF: 'turn_off',
SERVICE_VOLUME_UP: 'volume_up',
SERVICE_VOLUME_DOWN: 'volume_down',
SERVICE_MEDIA_PLAY_PAUSE: 'media_play_pause',
SERVICE_MEDIA_PLAY: 'media_play',
SERVICE_MEDIA_PAUSE: 'media_pause',
SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
SERVICE_MEDIA_PREV_TRACK: 'media_prev_track',
}
hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data)
def setup(hass, config):
@ -180,35 +211,56 @@ def setup(hass, config):
for service in SERVICE_TO_METHOD:
hass.services.register(DOMAIN, service, media_player_service_handler)
def volume_set_service(service, volume):
def volume_set_service(service):
""" Set specified volume on the media player. """
target_players = component.extract_from_service(service)
if ATTR_MEDIA_VOLUME_LEVEL not in service.data:
return
volume = service.data[ATTR_MEDIA_VOLUME_LEVEL]
for player in target_players:
player.volume_set(volume)
player.set_volume_level(volume)
if player.should_poll:
player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_VOLUME_SET,
lambda service:
volume_set_service(
service, service.data.get('volume')))
hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service)
def volume_mute_service(service, mute):
def volume_mute_service(service):
""" Mute (true) or unmute (false) the media player. """
target_players = component.extract_from_service(service)
if ATTR_MEDIA_VOLUME_MUTED not in service.data:
return
mute = service.data[ATTR_MEDIA_VOLUME_MUTED]
for player in target_players:
player.volume_mute(mute)
player.mute_volume(mute)
if player.should_poll:
player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE,
lambda service:
volume_mute_service(
service, service.data.get('mute')))
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, volume_mute_service)
def media_seek_service(service):
""" Seek to a position. """
target_players = component.extract_from_service(service)
if ATTR_MEDIA_SEEK_POSITION not in service.data:
return
position = service.data[ATTR_MEDIA_SEEK_POSITION]
for player in target_players:
player.seek(position)
if player.should_poll:
player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service)
def play_youtube_video_service(service, media_id):
""" Plays specified media_id on the media player. """
@ -239,51 +291,215 @@ def setup(hass, config):
class MediaPlayerDevice(Entity):
""" ABC for media player devices. """
# pylint: disable=too-many-public-methods,no-self-use
# Implement these for your media player
@property
def state(self):
""" State of the player. """
return STATE_UNKNOWN
@property
def volume_level(self):
""" Volume level of the media player (0..1). """
return None
@property
def is_volume_muted(self):
""" Boolean if volume is currently muted. """
return None
@property
def media_content_id(self):
""" Content ID of current playing media. """
return None
@property
def media_content_type(self):
""" Content type of current playing media. """
return None
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return None
@property
def media_image_url(self):
""" Image url of current playing media. """
return None
@property
def media_title(self):
""" Title of current playing media. """
return None
@property
def media_artist(self):
""" Artist of current playing media. (Music track only) """
return None
@property
def media_album_name(self):
""" Album name of current playing media. (Music track only) """
return None
@property
def media_album_artist(self):
""" Album arist of current playing media. (Music track only) """
return None
@property
def media_track(self):
""" Track number of current playing media. (Music track only) """
return None
@property
def media_series_title(self):
""" Series title of current playing media. (TV Show only)"""
return None
@property
def media_season(self):
""" Season of current playing media. (TV Show only) """
return None
@property
def media_episode(self):
""" Episode of current playing media. (TV Show only) """
return None
@property
def app_id(self):
""" ID of the current running app. """
return None
@property
def app_name(self):
""" Name of the current running app. """
return None
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return 0
@property
def device_state_attributes(self):
""" Extra attributes a device wants to expose. """
return None
def turn_on(self):
""" turn media player on. """
pass
""" turn the media player on. """
raise NotImplementedError()
def turn_off(self):
""" turn media player off. """
pass
""" turn the media player off. """
raise NotImplementedError()
def volume_up(self):
""" volume_up media player. """
pass
def mute_volume(self, mute):
""" mute the volume. """
raise NotImplementedError()
def volume_down(self):
""" volume_down media player. """
pass
def volume_mute(self, mute):
""" mute (true) or unmute (false) media player. """
pass
def volume_set(self, volume):
""" set volume level of media player. """
pass
def media_play_pause(self):
""" media_play_pause media player. """
pass
def set_volume_level(self, volume):
""" set volume level, range 0..1. """
raise NotImplementedError()
def media_play(self):
""" media_play media player. """
pass
""" Send play commmand. """
raise NotImplementedError()
def media_pause(self):
""" media_pause media player. """
pass
""" Send pause command. """
raise NotImplementedError()
def media_prev_track(self):
""" media_prev_track media player. """
pass
def media_previous_track(self):
""" Send previous track command. """
raise NotImplementedError()
def media_next_track(self):
""" media_next_track media player. """
pass
""" Send next track command. """
raise NotImplementedError()
def media_seek(self, position):
""" Send seek command. """
raise NotImplementedError()
def play_youtube(self, media_id):
""" Plays a YouTube media. """
pass
raise NotImplementedError()
# No need to overwrite these.
@property
def support_pause(self):
""" Boolean if pause is supported. """
return bool(self.supported_media_commands & SUPPORT_PAUSE)
@property
def support_seek(self):
""" Boolean if seek is supported. """
return bool(self.supported_media_commands & SUPPORT_SEEK)
@property
def support_volume_set(self):
""" Boolean if setting volume is supported. """
return bool(self.supported_media_commands & SUPPORT_VOLUME_SET)
@property
def support_volume_mute(self):
""" Boolean if muting volume is supported. """
return bool(self.supported_media_commands & SUPPORT_VOLUME_MUTE)
@property
def support_previous_track(self):
""" Boolean if previous track command supported. """
return bool(self.supported_media_commands & SUPPORT_PREVIOUS_TRACK)
@property
def support_next_track(self):
""" Boolean if next track command supported. """
return bool(self.supported_media_commands & SUPPORT_NEXT_TRACK)
@property
def support_youtube(self):
""" Boolean if YouTube is supported. """
return bool(self.supported_media_commands & SUPPORT_YOUTUBE)
def volume_up(self):
""" volume_up media player. """
if self.volume_level < 1:
self.set_volume_level(min(1, self.volume_level + .1))
def volume_down(self):
""" volume_down media player. """
if self.volume_level > 0:
self.set_volume_level(max(0, self.volume_level - .1))
def media_play_pause(self):
""" media_play_pause media player. """
if self.state == STATE_PLAYING:
self.media_pause()
else:
self.media_play()
@property
def state_attributes(self):
""" Return the state attributes. """
if self.state == STATE_OFF:
state_attr = {}
else:
state_attr = {
attr: getattr(self, attr) for attr
in ATTR_TO_PROPERTY if getattr(self, attr)
}
if self.media_image_url:
state_attr[ATTR_ENTITY_PICTURE] = self.media_image_url
device_attr = self.device_state_attributes
if device_attr:
state_attr.update(device_attr)
return state_attr

View File

@ -14,18 +14,21 @@ try:
except ImportError:
pychromecast = None
from homeassistant.const import ATTR_ENTITY_PICTURE
from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_IDLE, STATE_OFF,
STATE_UNKNOWN)
# ATTR_MEDIA_ALBUM, ATTR_MEDIA_IMAGE_URL,
# ATTR_MEDIA_ARTIST,
from homeassistant.components.media_player import (
MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE, ATTR_MEDIA_TITLE,
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_DURATION,
ATTR_MEDIA_VOLUME, ATTR_MEDIA_IS_VOLUME_MUTED,
MEDIA_STATE_PLAYING, MEDIA_STATE_PAUSED, MEDIA_STATE_STOPPED,
MEDIA_STATE_UNKNOWN)
MediaPlayerDevice,
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_YOUTUBE,
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png'
SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_YOUTUBE
# pylint: disable=unused-argument
@ -61,6 +64,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class CastDevice(MediaPlayerDevice):
""" Represents a Cast device on the network. """
# pylint: disable=too-many-public-methods
def __init__(self, host):
self.cast = pychromecast.Chromecast(host)
self.youtube = youtube.YouTubeController()
@ -73,6 +78,8 @@ class CastDevice(MediaPlayerDevice):
self.cast_status = self.cast.status
self.media_status = self.cast.media_controller.status
# Entity properties and methods
@property
def should_poll(self):
return False
@ -82,57 +89,121 @@ class CastDevice(MediaPlayerDevice):
""" Returns the name of the device. """
return self.cast.device.friendly_name
# MediaPlayerDevice properties and methods
@property
def state(self):
""" Returns the state of the device. """
if self.cast.is_idle:
return STATE_NO_APP
""" State of the player. """
if self.media_status is None:
return STATE_UNKNOWN
elif self.media_status.player_is_playing:
return STATE_PLAYING
elif self.media_status.player_is_paused:
return STATE_PAUSED
elif self.media_status.player_is_idle:
return STATE_IDLE
elif self.cast.is_idle:
return STATE_OFF
else:
return self.cast.app_display_name
return STATE_UNKNOWN
@property
def media_state(self):
""" Returns the media state. """
media_controller = self.cast.media_controller
if media_controller.is_playing:
return MEDIA_STATE_PLAYING
elif media_controller.is_paused:
return MEDIA_STATE_PAUSED
elif media_controller.is_idle:
return MEDIA_STATE_STOPPED
else:
return MEDIA_STATE_UNKNOWN
def volume_level(self):
""" Volume level of the media player (0..1). """
return self.cast_status.volume_level if self.cast_status else None
@property
def state_attributes(self):
""" Returns the state attributes. """
cast_status = self.cast_status
media_status = self.media_status
media_controller = self.cast.media_controller
def is_volume_muted(self):
""" Boolean if volume is currently muted. """
return self.cast_status.volume_muted if self.cast_status else None
state_attr = {
ATTR_MEDIA_STATE: self.media_state,
'application_id': self.cast.app_id,
}
@property
def media_content_id(self):
""" Content ID of current playing media. """
return self.media_status.content_id if self.media_status else None
if cast_status:
state_attr[ATTR_MEDIA_VOLUME] = cast_status.volume_level
state_attr[ATTR_MEDIA_IS_VOLUME_MUTED] = cast_status.volume_muted
@property
def media_content_type(self):
""" Content type of current playing media. """
if self.media_status is None:
return None
elif self.media_status.media_is_tvshow:
return MEDIA_TYPE_TVSHOW
elif self.media_status.media_is_movie:
return MEDIA_TYPE_VIDEO
elif self.media_status.media_is_musictrack:
return MEDIA_TYPE_MUSIC
return None
if media_status.content_id:
state_attr[ATTR_MEDIA_CONTENT_ID] = media_status.content_id
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return self.media_status.duration if self.media_status else None
if media_status.duration:
state_attr[ATTR_MEDIA_DURATION] = media_status.duration
@property
def media_image_url(self):
""" Image url of current playing media. """
if self.media_status is None:
return None
if media_controller.title:
state_attr[ATTR_MEDIA_TITLE] = media_controller.title
images = self.media_status.images
if media_controller.thumbnail:
state_attr[ATTR_ENTITY_PICTURE] = media_controller.thumbnail
return images[0].url if images else None
return state_attr
@property
def media_title(self):
""" Title of current playing media. """
return self.media_status.title if self.media_status else None
@property
def media_artist(self):
""" Artist of current playing media. (Music track only) """
return self.media_status.artist if self.media_status else None
@property
def media_album(self):
""" Album of current playing media. (Music track only) """
return self.media_status.album_name if self.media_status else None
@property
def media_album_artist(self):
""" Album arist of current playing media. (Music track only) """
return self.media_status.album_artist if self.media_status else None
@property
def media_track(self):
""" Track number of current playing media. (Music track only) """
return self.media_status.track if self.media_status else None
@property
def media_series_title(self):
""" Series title of current playing media. (TV Show only)"""
return self.media_status.series_title if self.media_status else None
@property
def media_season(self):
""" Season of current playing media. (TV Show only) """
return self.media_status.season if self.media_status else None
@property
def media_episode(self):
""" Episode of current playing media. (TV Show only) """
return self.media_status.episode if self.media_status else None
@property
def app_id(self):
""" ID of the current running app. """
return self.cast.app_id
@property
def app_name(self):
""" Name of the current running app. """
return self.cast.app_display_name
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return SUPPORT_CAST
def turn_on(self):
""" Turns on the ChromeCast. """
@ -145,57 +216,42 @@ class CastDevice(MediaPlayerDevice):
CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED)
def turn_off(self):
""" Service to exit any running app on the specimedia player ChromeCast and
shows idle screen. Will quit all ChromeCasts if nothing specified.
"""
""" Turns Chromecast off. """
self.cast.quit_app()
def volume_up(self):
""" Service to send the chromecast the command for volume up. """
self.cast.volume_up()
def volume_down(self):
""" Service to send the chromecast the command for volume down. """
self.cast.volume_down()
def volume_mute(self, mute):
""" Set media player to mute volume. """
def mute_volume(self, mute):
""" mute the volume. """
self.cast.set_volume_muted(mute)
def volume_set(self, volume):
""" Set media player volume, range of volume 0..1 """
def set_volume_level(self, volume):
""" set volume level, range 0..1. """
self.cast.set_volume(volume)
def media_play_pause(self):
""" Service to send the chromecast the command for play/pause. """
media_state = self.media_state
if media_state in (MEDIA_STATE_STOPPED, MEDIA_STATE_PAUSED):
self.cast.media_controller.play()
elif media_state == MEDIA_STATE_PLAYING:
self.cast.media_controller.pause()
def media_play(self):
""" Service to send the chromecast the command for play/pause. """
if self.media_state in (MEDIA_STATE_STOPPED, MEDIA_STATE_PAUSED):
self.cast.media_controller.play()
""" Send play commmand. """
self.cast.media_controller.play()
def media_pause(self):
""" Service to send the chromecast the command for play/pause. """
if self.media_state == MEDIA_STATE_PLAYING:
self.cast.media_controller.pause()
""" Send pause command. """
self.cast.media_controller.pause()
def media_prev_track(self):
""" media_prev_track media player. """
def media_previous_track(self):
""" Send previous track command. """
self.cast.media_controller.rewind()
def media_next_track(self):
""" media_next_track media player. """
""" Send next track command. """
self.cast.media_controller.skip()
def play_youtube_video(self, video_id):
""" Plays specified video_id on the Chromecast's YouTube channel. """
self.youtube.play_video(video_id)
def media_seek(self, position):
""" Seek the media to a specific location. """
self.cast.media_controller.seek(position)
def play_youtube(self, media_id):
""" Plays a YouTube media. """
self.youtube.play_video(media_id)
# implementation of chromecast status_listener methods
def new_cast_status(self, status):
""" Called when a new cast status is received. """

View File

@ -5,121 +5,334 @@ homeassistant.components.media_player.demo
Demo implementation of the media player.
"""
from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_OFF)
from homeassistant.components.media_player import (
MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE,
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_TITLE, ATTR_MEDIA_DURATION,
ATTR_MEDIA_VOLUME, MEDIA_STATE_PLAYING, MEDIA_STATE_STOPPED,
YOUTUBE_COVER_URL_FORMAT, ATTR_MEDIA_IS_VOLUME_MUTED)
from homeassistant.const import ATTR_ENTITY_PICTURE
MediaPlayerDevice, YOUTUBE_COVER_URL_FORMAT,
MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_YOUTUBE,
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PREVIOUS_TRACK,
SUPPORT_NEXT_TRACK)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the cast platform. """
add_devices([
DemoMediaPlayer(
DemoYoutubePlayer(
'Living Room', 'eyU3bRy2x44',
'♥♥ The Best Fireplace Video (3 hours)'),
DemoMediaPlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours')
DemoYoutubePlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours'),
DemoMusicPlayer(), DemoTVShowPlayer(),
])
class DemoMediaPlayer(MediaPlayerDevice):
""" A Demo media player that only supports YouTube. """
YOUTUBE_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_YOUTUBE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
def __init__(self, name, youtube_id=None, media_title=None):
MUSIC_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF
NETFLIX_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
class AbstractDemoPlayer(MediaPlayerDevice):
""" Base class for demo media players. """
# We only implement the methods that we support
# pylint: disable=abstract-method
def __init__(self, name):
self._name = name
self.is_playing = youtube_id is not None
self.youtube_id = youtube_id
self.media_title = media_title
self.volume = 1.0
self.is_volume_muted = False
self._player_state = STATE_PLAYING
self._volume_level = 1.0
self._volume_muted = False
@property
def should_poll(self):
""" No polling needed for a demo componentn. """
""" We will push an update after each command. """
return False
@property
def name(self):
""" Returns the name of the device. """
""" Name of the media player. """
return self._name
@property
def state(self):
""" Returns the state of the device. """
return STATE_NO_APP if self.youtube_id is None else "YouTube"
""" State of the player. """
return self._player_state
@property
def state_attributes(self):
""" Returns the state attributes. """
if self.youtube_id is None:
return
def volume_level(self):
""" Volume level of the media player (0..1). """
return self._volume_level
state_attr = {
ATTR_MEDIA_CONTENT_ID: self.youtube_id,
ATTR_MEDIA_TITLE: self.media_title,
ATTR_MEDIA_DURATION: 100,
ATTR_MEDIA_VOLUME: self.volume,
ATTR_MEDIA_IS_VOLUME_MUTED: self.is_volume_muted,
ATTR_ENTITY_PICTURE:
YOUTUBE_COVER_URL_FORMAT.format(self.youtube_id)
}
if self.is_playing:
state_attr[ATTR_MEDIA_STATE] = MEDIA_STATE_PLAYING
else:
state_attr[ATTR_MEDIA_STATE] = MEDIA_STATE_STOPPED
return state_attr
@property
def is_volume_muted(self):
""" Boolean if volume is currently muted. """
return self._volume_muted
def turn_on(self):
""" turn_off media player. """
self.youtube_id = "eyU3bRy2x44"
self.is_playing = False
""" turn the media player on. """
self._player_state = STATE_PLAYING
self.update_ha_state()
def turn_off(self):
""" turn_off media player. """
self.youtube_id = None
self.is_playing = False
""" turn the media player off. """
self._player_state = STATE_OFF
self.update_ha_state()
def volume_up(self):
""" volume_up media player. """
if self.volume < 1:
self.volume += 0.1
self.update_ha_state()
def volume_down(self):
""" volume_down media player. """
if self.volume > 0:
self.volume -= 0.1
self.update_ha_state()
def volume_mute(self, mute):
""" mute (true) or unmute (false) media player. """
self.is_volume_muted = mute
def mute_volume(self, mute):
""" mute the volume. """
self._volume_muted = mute
self.update_ha_state()
def media_play_pause(self):
""" media_play_pause media player. """
self.is_playing = not self.is_playing
def set_volume_level(self, volume):
""" set volume level, range 0..1. """
self._volume_level = volume
self.update_ha_state()
def media_play(self):
""" media_play media player. """
self.is_playing = True
""" Send play commmand. """
self._player_state = STATE_PLAYING
self.update_ha_state()
def media_pause(self):
""" media_pause media player. """
self.is_playing = False
""" Send pause command. """
self._player_state = STATE_PAUSED
self.update_ha_state()
class DemoYoutubePlayer(AbstractDemoPlayer):
""" A Demo media player that only supports YouTube. """
# We only implement the methods that we support
# pylint: disable=abstract-method
def __init__(self, name, youtube_id=None, media_title=None):
super().__init__(name)
self.youtube_id = youtube_id
self._media_title = media_title
@property
def media_content_id(self):
""" Content ID of current playing media. """
return self.youtube_id
@property
def media_content_type(self):
""" Content type of current playing media. """
return MEDIA_TYPE_VIDEO
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return 360
@property
def media_image_url(self):
""" Image url of current playing media. """
return YOUTUBE_COVER_URL_FORMAT.format(self.youtube_id)
@property
def media_title(self):
""" Title of current playing media. """
return self._media_title
@property
def app_name(self):
""" Current running app. """
return "YouTube"
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return YOUTUBE_PLAYER_SUPPORT
def play_youtube(self, media_id):
""" Plays a YouTube media. """
self.youtube_id = media_id
self.media_title = 'Demo media title'
self.is_playing = True
self._media_title = 'some YouTube video'
self.update_ha_state()
class DemoMusicPlayer(AbstractDemoPlayer):
""" A Demo media player that only supports YouTube. """
# We only implement the methods that we support
# pylint: disable=abstract-method
tracks = [
('Technohead', 'I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)'),
('Paul Elstak', 'Luv U More'),
('Dune', 'Hardcore Vibes'),
('Nakatomi', 'Children Of The Night'),
('Party Animals',
'Have You Ever Been Mellow? (Flamman & Abraxas Radio Mix)'),
('Rob G.*', 'Ecstasy, You Got What I Need'),
('Lipstick', "I'm A Raver"),
('4 Tune Fairytales', 'My Little Fantasy (Radio Edit)'),
('Prophet', "The Big Boys Don't Cry"),
('Lovechild', 'All Out Of Love (DJ Weirdo & Sim Remix)'),
('Stingray & Sonic Driver', 'Cold As Ice (El Bruto Remix)'),
('Highlander', 'Hold Me Now (Bass-D & King Matthew Remix)'),
('Juggernaut', 'Ruffneck Rules Da Artcore Scene (12" Edit)'),
('Diss Reaction', 'Jiiieehaaaa '),
('Flamman And Abraxas', 'Good To Go (Radio Mix)'),
('Critical Mass', 'Dancing Together'),
('Charly Lownoise & Mental Theo',
'Ultimate Sex Track (Bass-D & King Matthew Remix)'),
]
def __init__(self):
super().__init__('Walkman')
self._cur_track = 0
@property
def media_content_id(self):
""" Content ID of current playing media. """
return 'bounzz-1'
@property
def media_content_type(self):
""" Content type of current playing media. """
return MEDIA_TYPE_MUSIC
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return 213
@property
def media_image_url(self):
""" Image url of current playing media. """
return 'https://graph.facebook.com/107771475912710/picture'
@property
def media_title(self):
""" Title of current playing media. """
return self.tracks[self._cur_track][1]
@property
def media_artist(self):
""" Artist of current playing media. (Music track only) """
return self.tracks[self._cur_track][0]
@property
def media_album_name(self):
""" Album of current playing media. (Music track only) """
# pylint: disable=no-self-use
return "Bounzz"
@property
def media_track(self):
""" Track number of current playing media. (Music track only) """
return self._cur_track + 1
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
support = MUSIC_PLAYER_SUPPORT
if self._cur_track > 1:
support |= SUPPORT_PREVIOUS_TRACK
if self._cur_track < len(self.tracks)-1:
support |= SUPPORT_NEXT_TRACK
return support
def media_previous_track(self):
""" Send previous track command. """
if self._cur_track > 0:
self._cur_track -= 1
self.update_ha_state()
def media_next_track(self):
""" Send next track command. """
if self._cur_track < len(self.tracks)-1:
self._cur_track += 1
self.update_ha_state()
class DemoTVShowPlayer(AbstractDemoPlayer):
""" A Demo media player that only supports YouTube. """
# We only implement the methods that we support
# pylint: disable=abstract-method
def __init__(self):
super().__init__('Lounge room')
self._cur_episode = 1
self._episode_count = 13
@property
def media_content_id(self):
""" Content ID of current playing media. """
return 'house-of-cards-1'
@property
def media_content_type(self):
""" Content type of current playing media. """
return MEDIA_TYPE_TVSHOW
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return 3600
@property
def media_image_url(self):
""" Image url of current playing media. """
return 'https://graph.facebook.com/HouseofCards/picture'
@property
def media_title(self):
""" Title of current playing media. """
return 'Chapter {}'.format(self._cur_episode)
@property
def media_series_title(self):
""" Series title of current playing media. (TV Show only)"""
return 'House of Cards'
@property
def media_season(self):
""" Season of current playing media. (TV Show only) """
return 1
@property
def media_episode(self):
""" Episode of current playing media. (TV Show only) """
return self._cur_episode
@property
def app_name(self):
""" Current running app. """
return "Netflix"
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
support = NETFLIX_PLAYER_SUPPORT
if self._cur_episode > 1:
support |= SUPPORT_PREVIOUS_TRACK
if self._cur_episode < self._episode_count:
support |= SUPPORT_NEXT_TRACK
return support
def media_previous_track(self):
""" Send previous track command. """
if self._cur_episode > 1:
self._cur_episode -= 1
self.update_ha_state()
def media_next_track(self):
""" Send next track command. """
if self._cur_episode < self._episode_count:
self._cur_episode += 1
self.update_ha_state()

View File

@ -32,16 +32,28 @@ Location of your Music Player Daemon.
import logging
import socket
try:
import mpd
except ImportError:
mpd = None
from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_OFF)
from homeassistant.components.media_player import (
MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE,
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_TITLE, ATTR_MEDIA_ARTIST,
ATTR_MEDIA_ALBUM, ATTR_MEDIA_DATE, ATTR_MEDIA_DURATION,
ATTR_MEDIA_VOLUME, MEDIA_STATE_PAUSED, MEDIA_STATE_PLAYING,
MEDIA_STATE_STOPPED, MEDIA_STATE_UNKNOWN)
MediaPlayerDevice,
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_TURN_OFF,
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
MEDIA_TYPE_MUSIC)
_LOGGER = logging.getLogger(__name__)
SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the MPD platform. """
@ -50,10 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
port = config.get('port', 6600)
location = config.get('location', 'MPD')
try:
from mpd import MPDClient
except ImportError:
if mpd is None:
_LOGGER.exception(
"Unable to import mpd2. "
"Did you maybe not install the 'python-mpd2' package?")
@ -62,7 +71,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
# pylint: disable=no-member
try:
mpd_client = MPDClient()
mpd_client = mpd.MPDClient()
mpd_client.connect(daemon, port)
mpd_client.close()
mpd_client.disconnect()
@ -73,110 +82,112 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return False
mpd = []
mpd.append(MpdDevice(daemon, port, location))
add_devices(mpd)
add_devices([MpdDevice(daemon, port, location)])
class MpdDevice(MediaPlayerDevice):
""" Represents a MPD server. """
def __init__(self, server, port, location):
from mpd import MPDClient
# MPD confuses pylint
# pylint: disable=no-member, abstract-method
def __init__(self, server, port, location):
self.server = server
self.port = port
self._name = location
self.state_attr = {ATTR_MEDIA_STATE: MEDIA_STATE_STOPPED}
self.status = None
self.currentsong = None
self.client = MPDClient()
self.client = mpd.MPDClient()
self.client.timeout = 10
self.client.idletimeout = None
self.client.connect(self.server, self.port)
self.update()
def update(self):
try:
self.status = self.client.status()
self.currentsong = self.client.currentsong()
except mpd.ConnectionError:
self.client.connect(self.server, self.port)
self.status = self.client.status()
self.currentsong = self.client.currentsong()
@property
def name(self):
""" Returns the name of the device. """
return self._name
# pylint: disable=no-member
@property
def state(self):
""" Returns the state of the device. """
status = self.client.status()
if status is None:
return STATE_NO_APP
else:
return self.client.currentsong()['artist']
@property
def media_state(self):
""" Returns the media state. """
media_controller = self.client.status()
if media_controller['state'] == 'play':
return MEDIA_STATE_PLAYING
elif media_controller['state'] == 'pause':
return MEDIA_STATE_PAUSED
elif media_controller['state'] == 'stop':
return MEDIA_STATE_STOPPED
if self.status['state'] == 'play':
return STATE_PLAYING
elif self.status['state'] == 'pause':
return STATE_PAUSED
else:
return MEDIA_STATE_UNKNOWN
return STATE_OFF
# pylint: disable=no-member
@property
def state_attributes(self):
""" Returns the state attributes. """
status = self.client.status()
current_song = self.client.currentsong()
def media_content_id(self):
""" Content ID of current playing media. """
return self.currentsong['id']
if not status and not current_song:
state_attr = {}
@property
def media_content_type(self):
""" Content type of current playing media. """
return MEDIA_TYPE_MUSIC
if current_song['id']:
state_attr[ATTR_MEDIA_CONTENT_ID] = current_song['id']
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
# Time does not exist for streams
return self.currentsong.get('time')
if current_song['date']:
state_attr[ATTR_MEDIA_DATE] = current_song['date']
@property
def media_title(self):
""" Title of current playing media. """
return self.currentsong['title']
if current_song['title']:
state_attr[ATTR_MEDIA_TITLE] = current_song['title']
@property
def media_artist(self):
""" Artist of current playing media. (Music track only) """
return self.currentsong.get('artist')
if current_song['time']:
state_attr[ATTR_MEDIA_DURATION] = current_song['time']
@property
def media_album_name(self):
""" Album of current playing media. (Music track only) """
return self.currentsong.get('album')
if current_song['artist']:
state_attr[ATTR_MEDIA_ARTIST] = current_song['artist']
@property
def volume_level(self):
return int(self.status['volume'])/100
if current_song['album']:
state_attr[ATTR_MEDIA_ALBUM] = current_song['album']
state_attr[ATTR_MEDIA_VOLUME] = status['volume']
return state_attr
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return SUPPORT_MPD
def turn_off(self):
""" Service to exit the running MPD. """
self.client.stop()
def set_volume_level(self, volume):
""" Sets volume """
self.client.setvol(int(volume * 100))
def volume_up(self):
""" Service to send the MPD the command for volume up. """
current_volume = self.client.status()['volume']
current_volume = int(self.status['volume'])
if int(current_volume) <= 100:
self.client.setvol(int(current_volume) + 5)
if current_volume <= 100:
self.client.setvol(current_volume + 5)
def volume_down(self):
""" Service to send the MPD the command for volume down. """
current_volume = self.client.status()['volume']
current_volume = int(self.status['volume'])
if int(current_volume) >= 0:
self.client.setvol(int(current_volume) - 5)
def media_play_pause(self):
""" Service to send the MPD the command for play/pause. """
self.client.pause()
if current_volume >= 0:
self.client.setvol(current_volume - 5)
def media_play(self):
""" Service to send the MPD the command for play/pause. """
@ -190,6 +201,6 @@ class MpdDevice(MediaPlayerDevice):
""" Service to send the MPD the command for next track. """
self.client.next()
def media_prev_track(self):
def media_previous_track(self):
""" Service to send the MPD the command for previous track. """
self.client.previous()

View File

@ -40,6 +40,9 @@ STATE_NOT_HOME = 'not_home'
STATE_UNKNOWN = "unknown"
STATE_OPEN = 'open'
STATE_CLOSED = 'closed'
STATE_PLAYING = 'playing'
STATE_PAUSED = 'paused'
STATE_IDLE = 'idle'
# #### STATE AND EVENT ATTRIBUTES ####
# Contains current time for a TIME_CHANGED event
@ -104,7 +107,8 @@ SERVICE_MEDIA_PLAY_PAUSE = "media_play_pause"
SERVICE_MEDIA_PLAY = "media_play"
SERVICE_MEDIA_PAUSE = "media_pause"
SERVICE_MEDIA_NEXT_TRACK = "media_next_track"
SERVICE_MEDIA_PREV_TRACK = "media_prev_track"
SERVICE_MEDIA_PREVIOUS_TRACK = "media_previous_track"
SERVICE_MEDIA_SEEK = "media_seek"
# #### API / REMOTE ####
SERVER_PORT = 8123

View File

@ -18,7 +18,7 @@ phue>=0.8
ledcontroller>=1.0.7
# Chromecast bindings (media_player.cast)
pychromecast>=0.6.4
pychromecast>=0.6.5
# Keyboard (keyboard)
pyuserinput>=0.1.9

View File

@ -10,9 +10,10 @@ import unittest
import homeassistant as ha
from homeassistant.const import (
STATE_OFF,
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN,
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK, ATTR_ENTITY_ID)
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, ATTR_ENTITY_ID)
import homeassistant.components.media_player as media_player
from helpers import mock_service
@ -29,7 +30,7 @@ class TestMediaPlayer(unittest.TestCase):
self.hass = ha.HomeAssistant()
self.test_entity = media_player.ENTITY_ID_FORMAT.format('living_room')
self.hass.states.set(self.test_entity, media_player.STATE_NO_APP)
self.hass.states.set(self.test_entity, STATE_OFF)
self.test_entity2 = media_player.ENTITY_ID_FORMAT.format('bedroom')
self.hass.states.set(self.test_entity2, "YouTube")
@ -56,7 +57,7 @@ class TestMediaPlayer(unittest.TestCase):
SERVICE_MEDIA_PLAY: media_player.media_play,
SERVICE_MEDIA_PAUSE: media_player.media_pause,
SERVICE_MEDIA_NEXT_TRACK: media_player.media_next_track,
SERVICE_MEDIA_PREV_TRACK: media_player.media_prev_track
SERVICE_MEDIA_PREVIOUS_TRACK: media_player.media_previous_track
}
for service_name, service_method in services.items():