diff --git a/src/panels/lovelace/card-features/hui-media-player-playback-card-feature.ts b/src/panels/lovelace/card-features/hui-media-player-playback-card-feature.ts
new file mode 100644
index 0000000000..2af417317c
--- /dev/null
+++ b/src/panels/lovelace/card-features/hui-media-player-playback-card-feature.ts
@@ -0,0 +1,323 @@
+import type { PropertyValues } from "lit";
+import { html, LitElement, nothing } from "lit";
+import { customElement, property, state } from "lit/decorators";
+import {
+ mdiPause,
+ mdiPlay,
+ mdiPlayPause,
+ mdiPower,
+ mdiSkipNext,
+ mdiSkipPrevious,
+ mdiStop,
+} from "@mdi/js";
+import { computeDomain } from "../../../common/entity/compute_domain";
+import type { HomeAssistant } from "../../../types";
+import type { LovelaceCardFeature } from "../types";
+import { cardFeatureStyles } from "./common/card-feature-styles";
+import type {
+ LovelaceCardFeatureContext,
+ MediaPlayerPlaybackCardFeatureConfig,
+} from "./types";
+import type {
+ ControlButton,
+ MediaPlayerEntity,
+} from "../../../data/media-player";
+import { MediaPlayerEntityFeature } from "../../../data/media-player";
+import { supportsFeature } from "../../../common/entity/supports-feature";
+import { stateActive } from "../../../common/entity/state_active";
+import { isUnavailableState } from "../../../data/entity";
+import { hasConfigChanged } from "../common/has-changed";
+import "../../../components/ha-control-button-group";
+import "../../../components/ha-control-button";
+import "../../../components/ha-icon-button";
+import "../../../components/ha-icon";
+
+export const supportsMediaPlayerPlaybackCardFeature = (
+ hass: HomeAssistant,
+ context: LovelaceCardFeatureContext
+) => {
+ const stateObj = context.entity_id
+ ? hass.states[context.entity_id]
+ : undefined;
+ if (!stateObj) return false;
+ const domain = computeDomain(stateObj.entity_id);
+ return domain === "media_player";
+};
+
+@customElement("hui-media-player-playback-card-feature")
+class HuiMediaPlayerPlaybackCardFeature
+ extends LitElement
+ implements LovelaceCardFeature
+{
+ @property({ attribute: false }) public hass?: HomeAssistant;
+
+ @property({ attribute: false }) public context?: LovelaceCardFeatureContext;
+
+ @property({ attribute: false }) public color?: string;
+
+ @state() private _config?: MediaPlayerPlaybackCardFeatureConfig;
+
+ @state() private _narrow?: boolean = false;
+
+ private get _stateObj() {
+ if (!this.hass || !this.context || !this.context.entity_id) {
+ return undefined;
+ }
+ return this.hass.states[this.context.entity_id] as
+ | MediaPlayerEntity
+ | undefined;
+ }
+
+ static getStubConfig(): MediaPlayerPlaybackCardFeatureConfig {
+ return {
+ type: "media-player-playback",
+ };
+ }
+
+ public setConfig(config: MediaPlayerPlaybackCardFeatureConfig): void {
+ if (!config) {
+ throw new Error("Invalid configuration");
+ }
+ this._config = config;
+ }
+
+ public willUpdate(): void {
+ if (!this.hasUpdated) {
+ this._measureCard();
+ }
+ }
+
+ protected shouldUpdate(changedProps: PropertyValues): boolean {
+ const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
+ const entityId = this.context?.entity_id;
+ return (
+ hasConfigChanged(this, changedProps) ||
+ (changedProps.has("hass") &&
+ (!oldHass ||
+ !entityId ||
+ oldHass.states[entityId] !== this.hass!.states[entityId]))
+ );
+ }
+
+ protected render() {
+ if (
+ !this._config ||
+ !this.hass ||
+ !this.context ||
+ !supportsMediaPlayerPlaybackCardFeature(this.hass, this.context) ||
+ !this._stateObj
+ ) {
+ return nothing;
+ }
+
+ const buttons = this._computeButtons(this._stateObj);
+
+ return html`
+
+ ${supportsFeature(this._stateObj, MediaPlayerEntityFeature.TURN_OFF) &&
+ stateActive(this._stateObj)
+ ? html`
+
+
+
+ `
+ : ""}
+ ${supportsFeature(this._stateObj, MediaPlayerEntityFeature.TURN_ON) &&
+ !stateActive(this._stateObj) &&
+ !isUnavailableState(this._stateObj.state)
+ ? html`
+
+
+
+ `
+ : buttons.map(
+ (button) => html`
+
+
+
+ `
+ )}
+
+ `;
+ }
+
+ private _measureCard() {
+ if (!this.isConnected) {
+ return;
+ }
+ const host = (this.getRootNode() as ShadowRoot).host as
+ | HTMLElement
+ | undefined;
+ const width = host?.clientWidth ?? this.clientWidth ?? 0;
+ this._narrow = width < 300;
+ }
+
+ private _computeControlButton(stateObj: MediaPlayerEntity): ControlButton {
+ return stateObj.state === "on"
+ ? { icon: mdiPlayPause, action: "media_play_pause" }
+ : stateObj.state !== "playing"
+ ? { icon: mdiPlay, action: "media_play" }
+ : supportsFeature(stateObj, MediaPlayerEntityFeature.PAUSE)
+ ? { icon: mdiPause, action: "media_pause" }
+ : { icon: mdiStop, action: "media_stop" };
+ }
+
+ private _computeButtons(stateObj: MediaPlayerEntity): ControlButton[] {
+ const controlButton = this._computeControlButton(stateObj);
+ const assumedState = stateObj.attributes.assumed_state === true;
+
+ const controls: ControlButton[] = [];
+
+ if (
+ !this._narrow &&
+ (stateObj.state === "playing" || assumedState) &&
+ supportsFeature(stateObj, MediaPlayerEntityFeature.PREVIOUS_TRACK)
+ ) {
+ controls.push({ icon: mdiSkipPrevious, action: "media_previous_track" });
+ }
+
+ if (
+ !assumedState &&
+ ((stateObj.state === "playing" &&
+ (supportsFeature(stateObj, MediaPlayerEntityFeature.PAUSE) ||
+ supportsFeature(stateObj, MediaPlayerEntityFeature.STOP))) ||
+ ((stateObj.state === "paused" || stateObj.state === "idle") &&
+ supportsFeature(stateObj, MediaPlayerEntityFeature.PLAY)) ||
+ (stateObj.state === "on" &&
+ (supportsFeature(stateObj, MediaPlayerEntityFeature.PLAY) ||
+ supportsFeature(stateObj, MediaPlayerEntityFeature.PAUSE))))
+ ) {
+ controls.push({ icon: controlButton.icon, action: controlButton.action });
+ }
+
+ if (assumedState) {
+ if (supportsFeature(stateObj, MediaPlayerEntityFeature.PLAY)) {
+ controls.push({ icon: mdiPlay, action: "media_play" });
+ }
+
+ if (supportsFeature(stateObj, MediaPlayerEntityFeature.PAUSE)) {
+ controls.push({ icon: mdiPause, action: "media_pause" });
+ }
+
+ if (supportsFeature(stateObj, MediaPlayerEntityFeature.STOP)) {
+ controls.push({ icon: mdiStop, action: "media_stop" });
+ }
+ }
+
+ if (
+ (stateObj.state === "playing" || assumedState) &&
+ supportsFeature(stateObj, MediaPlayerEntityFeature.NEXT_TRACK)
+ ) {
+ controls.push({ icon: mdiSkipNext, action: "media_next_track" });
+ }
+
+ return controls;
+ }
+
+ private _togglePower(): void {
+ if (!this._stateObj) return;
+ this.hass!.callService(
+ "media_player",
+ stateActive(this._stateObj) ? "turn_off" : "turn_on",
+ {
+ entity_id: this._stateObj.entity_id,
+ }
+ );
+ }
+
+ private _action(e: Event): void {
+ const action = (e.currentTarget as HTMLElement).getAttribute("key");
+ if (!action) return;
+
+ switch (action) {
+ case "media_play_pause":
+ this._playPauseStop();
+ break;
+ case "media_play":
+ this._play();
+ break;
+ case "media_pause":
+ this._pause();
+ break;
+ case "media_stop":
+ this._stop();
+ break;
+ case "media_previous_track":
+ this._previousTrack();
+ break;
+ case "media_next_track":
+ this._nextTrack();
+ break;
+ }
+ }
+
+ private _playPauseStop(): void {
+ if (!this._stateObj) return;
+
+ const service =
+ this._stateObj.state !== "playing"
+ ? "media_play"
+ : supportsFeature(this._stateObj, MediaPlayerEntityFeature.PAUSE)
+ ? "media_pause"
+ : "media_stop";
+
+ this.hass!.callService("media_player", service, {
+ entity_id: this._stateObj.entity_id,
+ });
+ }
+
+ private _play(): void {
+ if (!this._stateObj) return;
+ this.hass!.callService("media_player", "media_play", {
+ entity_id: this._stateObj.entity_id,
+ });
+ }
+
+ private _pause(): void {
+ if (!this._stateObj) return;
+ this.hass!.callService("media_player", "media_pause", {
+ entity_id: this._stateObj.entity_id,
+ });
+ }
+
+ private _stop(): void {
+ if (!this._stateObj) return;
+ this.hass!.callService("media_player", "media_stop", {
+ entity_id: this._stateObj.entity_id,
+ });
+ }
+
+ private _previousTrack(): void {
+ if (!this._stateObj) return;
+ this.hass!.callService("media_player", "media_previous_track", {
+ entity_id: this._stateObj.entity_id,
+ });
+ }
+
+ private _nextTrack(): void {
+ if (!this._stateObj) return;
+ this.hass!.callService("media_player", "media_next_track", {
+ entity_id: this._stateObj.entity_id,
+ });
+ }
+
+ static styles = cardFeatureStyles;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-media-player-playback-card-feature": HuiMediaPlayerPlaybackCardFeature;
+ }
+}
diff --git a/src/panels/lovelace/card-features/types.ts b/src/panels/lovelace/card-features/types.ts
index fcb0b00190..27b2969703 100644
--- a/src/panels/lovelace/card-features/types.ts
+++ b/src/panels/lovelace/card-features/types.ts
@@ -39,6 +39,10 @@ export interface LockOpenDoorCardFeatureConfig {
type: "lock-open-door";
}
+export interface MediaPlayerPlaybackCardFeatureConfig {
+ type: "media-player-playback";
+}
+
export interface MediaPlayerVolumeSliderCardFeatureConfig {
type: "media-player-volume-slider";
}
@@ -242,6 +246,7 @@ export type LovelaceCardFeatureConfig =
| LightColorTempCardFeatureConfig
| LockCommandsCardFeatureConfig
| LockOpenDoorCardFeatureConfig
+ | MediaPlayerPlaybackCardFeatureConfig
| MediaPlayerVolumeSliderCardFeatureConfig
| NumericInputCardFeatureConfig
| SelectOptionsCardFeatureConfig
diff --git a/src/panels/lovelace/create-element/create-card-feature-element.ts b/src/panels/lovelace/create-element/create-card-feature-element.ts
index ebee9dded8..03fa268289 100644
--- a/src/panels/lovelace/create-element/create-card-feature-element.ts
+++ b/src/panels/lovelace/create-element/create-card-feature-element.ts
@@ -22,6 +22,7 @@ import "../card-features/hui-light-brightness-card-feature";
import "../card-features/hui-light-color-temp-card-feature";
import "../card-features/hui-lock-commands-card-feature";
import "../card-features/hui-lock-open-door-card-feature";
+import "../card-features/hui-media-player-playback-card-feature";
import "../card-features/hui-media-player-volume-slider-card-feature";
import "../card-features/hui-numeric-input-card-feature";
import "../card-features/hui-select-options-card-feature";
@@ -70,6 +71,7 @@ const TYPES = new Set([
"light-color-temp",
"lock-commands",
"lock-open-door",
+ "media-player-playback",
"media-player-volume-slider",
"numeric-input",
"select-options",
diff --git a/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts b/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts
index 26ac749f5d..c75c3721ad 100644
--- a/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts
@@ -42,6 +42,7 @@ import { supportsLightBrightnessCardFeature } from "../../card-features/hui-ligh
import { supportsLightColorTempCardFeature } from "../../card-features/hui-light-color-temp-card-feature";
import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature";
import { supportsLockOpenDoorCardFeature } from "../../card-features/hui-lock-open-door-card-feature";
+import { supportsMediaPlayerPlaybackCardFeature } from "../../card-features/hui-media-player-playback-card-feature";
import { supportsMediaPlayerVolumeSliderCardFeature } from "../../card-features/hui-media-player-volume-slider-card-feature";
import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature";
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
@@ -95,6 +96,7 @@ const UI_FEATURE_TYPES = [
"light-color-temp",
"lock-commands",
"lock-open-door",
+ "media-player-playback",
"media-player-volume-slider",
"numeric-input",
"select-options",
@@ -162,6 +164,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
"light-color-temp": supportsLightColorTempCardFeature,
"lock-commands": supportsLockCommandsCardFeature,
"lock-open-door": supportsLockOpenDoorCardFeature,
+ "media-player-playback": supportsMediaPlayerPlaybackCardFeature,
"media-player-volume-slider": supportsMediaPlayerVolumeSliderCardFeature,
"numeric-input": supportsNumericInputCardFeature,
"select-options": supportsSelectOptionsCardFeature,
diff --git a/src/translations/en.json b/src/translations/en.json
index 64b91afcf0..1b5ce7fe13 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -7905,6 +7905,9 @@
"lock-open-door": {
"label": "Lock open door"
},
+ "media-player-playback": {
+ "label": "Media player playback controls"
+ },
"media-player-volume-slider": {
"label": "Media player volume slider"
},