Use media selector for media_player.play_media (#26559)

This commit is contained in:
karwosts
2025-08-27 03:39:40 -07:00
committed by GitHub
parent c8be25dfc2
commit 673ca8ba4b
9 changed files with 91 additions and 158 deletions

View File

@@ -18,7 +18,6 @@ import { HaDeviceAction } from "../../../../src/panels/config/automation/action/
import { HaEventAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-event"; import { HaEventAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-event";
import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if"; import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if";
import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel"; import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel";
import { HaPlayMediaAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-play_media";
import { HaRepeatAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-repeat"; import { HaRepeatAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-repeat";
import { HaSequenceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-sequence"; import { HaSequenceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-sequence";
import { HaServiceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-service"; import { HaServiceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-service";
@@ -32,7 +31,6 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [
{ name: "Service", actions: [HaServiceAction.defaultConfig] }, { name: "Service", actions: [HaServiceAction.defaultConfig] },
{ name: "Condition", actions: [HaConditionAction.defaultConfig] }, { name: "Condition", actions: [HaConditionAction.defaultConfig] },
{ name: "Delay", actions: [HaDelayAction.defaultConfig] }, { name: "Delay", actions: [HaDelayAction.defaultConfig] },
{ name: "Play media", actions: [HaPlayMediaAction.defaultConfig] },
{ name: "Wait", actions: [HaWaitAction.defaultConfig] }, { name: "Wait", actions: [HaWaitAction.defaultConfig] },
{ name: "WaitForTrigger", actions: [HaWaitForTriggerAction.defaultConfig] }, { name: "WaitForTrigger", actions: [HaWaitForTriggerAction.defaultConfig] },
{ name: "Repeat", actions: [HaRepeatAction.defaultConfig] }, { name: "Repeat", actions: [HaRepeatAction.defaultConfig] },

View File

@@ -18,6 +18,7 @@ import "../ha-alert";
import "../ha-form/ha-form"; import "../ha-form/ha-form";
import type { SchemaUnion } from "../ha-form/types"; import type { SchemaUnion } from "../ha-form/types";
import { showMediaBrowserDialog } from "../media-player/show-media-browser-dialog"; import { showMediaBrowserDialog } from "../media-player/show-media-browser-dialog";
import { ensureArray } from "../../common/array/ensure-array";
const MANUAL_SCHEMA = [ const MANUAL_SCHEMA = [
{ name: "media_content_id", required: false, selector: { text: {} } }, { name: "media_content_id", required: false, selector: { text: {} } },
@@ -44,9 +45,19 @@ export class HaMediaSelector extends LitElement {
@property({ type: Boolean, reflect: true }) public required = true; @property({ type: Boolean, reflect: true }) public required = true;
@property({ attribute: false }) public context?: {
filter_entity?: string | string[];
};
@state() private _thumbnailUrl?: string | null; @state() private _thumbnailUrl?: string | null;
private _contextEntities: string[] | undefined;
willUpdate(changedProps: PropertyValues<this>) { willUpdate(changedProps: PropertyValues<this>) {
if (changedProps.has("context")) {
this._contextEntities = ensureArray(this.context?.filter_entity);
}
if (changedProps.has("value")) { if (changedProps.has("value")) {
const thumbnail = this.value?.metadata?.thumbnail; const thumbnail = this.value?.metadata?.thumbnail;
const oldThumbnail = (changedProps.get("value") as this["value"]) const oldThumbnail = (changedProps.get("value") as this["value"])
@@ -79,24 +90,25 @@ export class HaMediaSelector extends LitElement {
} }
protected render() { protected render() {
const stateObj = this.value?.entity_id const entityId = this._getActiveEntityId();
? this.hass.states[this.value.entity_id]
: undefined; const stateObj = entityId ? this.hass.states[entityId] : undefined;
const supportsBrowse = const supportsBrowse =
!this.value?.entity_id || !entityId ||
(stateObj && (stateObj &&
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)); supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA));
const hasAccept = this.selector?.media?.accept?.length; const hasAccept = this.selector?.media?.accept?.length;
return html` return html`
${hasAccept ${hasAccept ||
(this._contextEntities && this._contextEntities.length <= 1)
? nothing ? nothing
: html` : html`
<ha-entity-picker <ha-entity-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this.value?.entity_id} .value=${entityId}
.label=${this.label || .label=${this.label ||
this.hass.localize( this.hass.localize(
"ui.components.selectors.media.pick_media_player" "ui.components.selectors.media.pick_media_player"
@@ -104,8 +116,10 @@ export class HaMediaSelector extends LitElement {
.disabled=${this.disabled} .disabled=${this.disabled}
.helper=${this.helper} .helper=${this.helper}
.required=${this.required} .required=${this.required}
.hideClearIcon=${!!this._contextEntities}
.includeDomains=${INCLUDE_DOMAINS} .includeDomains=${INCLUDE_DOMAINS}
allow-custom-entity .includeEntities=${this._contextEntities}
.allowCustomEntity=${!this._contextEntities}
@value-changed=${this._entityChanged} @value-changed=${this._entityChanged}
></ha-entity-picker> ></ha-entity-picker>
`} `}
@@ -121,6 +135,7 @@ export class HaMediaSelector extends LitElement {
.data=${this.value || EMPTY_FORM} .data=${this.value || EMPTY_FORM}
.schema=${MANUAL_SCHEMA} .schema=${MANUAL_SCHEMA}
.computeLabel=${this._computeLabelCallback} .computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
></ha-form> ></ha-form>
` `
: html` : html`
@@ -133,7 +148,7 @@ export class HaMediaSelector extends LitElement {
: this.value.metadata?.title || this.value.media_content_id} : this.value.metadata?.title || this.value.media_content_id}
@click=${this._pickMedia} @click=${this._pickMedia}
@keydown=${this._handleKeyDown} @keydown=${this._handleKeyDown}
class=${this.disabled || (!this.value?.entity_id && !hasAccept) class=${this.disabled || (!entityId && !hasAccept)
? "disabled" ? "disabled"
: ""} : ""}
> >
@@ -193,21 +208,38 @@ export class HaMediaSelector extends LitElement {
): string => ): string =>
this.hass.localize(`ui.components.selectors.media.${schema.name}`); this.hass.localize(`ui.components.selectors.media.${schema.name}`);
private _computeHelperCallback = (
schema: SchemaUnion<typeof MANUAL_SCHEMA>
): string =>
this.hass.localize(`ui.components.selectors.media.${schema.name}_detail`);
private _entityChanged(ev: CustomEvent) { private _entityChanged(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
fireEvent(this, "value-changed", { if (this.context?.filter_entity) {
value: { fireEvent(this, "value-changed", {
entity_id: ev.detail.value, value: {
media_content_id: "", media_content_id: "",
media_content_type: "", media_content_type: "",
}, metadata: {
}); browse_entity_id: ev.detail.value,
},
},
});
} else {
fireEvent(this, "value-changed", {
value: {
entity_id: ev.detail.value,
media_content_id: "",
media_content_type: "",
},
});
}
} }
private _pickMedia() { private _pickMedia() {
showMediaBrowserDialog(this, { showMediaBrowserDialog(this, {
action: "pick", action: "pick",
entityId: this.value?.entity_id, entityId: this._getActiveEntityId(),
navigateIds: this.value?.metadata?.navigateIds, navigateIds: this.value?.metadata?.navigateIds,
accept: this.selector.media?.accept, accept: this.selector.media?.accept,
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => { mediaPickedCallback: (pickedMedia: MediaPickedEvent) => {
@@ -225,6 +257,9 @@ export class HaMediaSelector extends LitElement {
media_content_type: id.media_content_type, media_content_type: id.media_content_type,
media_content_id: id.media_content_id, media_content_id: id.media_content_id,
})), })),
...(this.context?.filter_entity
? { browse_entity_id: this._getActiveEntityId() }
: {}),
}, },
}, },
}); });
@@ -232,6 +267,15 @@ export class HaMediaSelector extends LitElement {
}); });
} }
private _getActiveEntityId(): string | undefined {
const metaId = this.value?.metadata?.browse_entity_id;
return (
this.value?.entity_id ||
(metaId && this._contextEntities?.includes(metaId) && metaId) ||
this._contextEntities?.[0]
);
}
private _handleKeyDown(ev: KeyboardEvent) { private _handleKeyDown(ev: KeyboardEvent) {
if (ev.key === "Enter" || ev.key === " ") { if (ev.key === "Enter" || ev.key === " ") {
ev.preventDefault(); ev.preventDefault();

View File

@@ -11,8 +11,6 @@ import {
union, union,
array, array,
assign, assign,
literal,
is,
boolean, boolean,
refine, refine,
} from "superstruct"; } from "superstruct";
@@ -68,17 +66,6 @@ export const serviceActionStruct: Describe<ServiceActionWithTemplate> = assign(
}) })
); );
const playMediaActionStruct: Describe<PlayMediaAction> = assign(
baseActionStruct,
object({
action: literal("media_player.play_media"),
target: optional(object({ entity_id: optional(string()) })),
entity_id: optional(string()),
data: object({ media_content_id: string(), media_content_type: string() }),
metadata: object(),
})
);
export interface ScriptEntity extends HassEntityBase { export interface ScriptEntity extends HassEntityBase {
attributes: HassEntityAttributeBase & { attributes: HassEntityAttributeBase & {
last_triggered: string; last_triggered: string;
@@ -182,14 +169,6 @@ export interface WaitForTriggerAction extends BaseAction {
continue_on_timeout?: boolean; continue_on_timeout?: boolean;
} }
export interface PlayMediaAction extends BaseAction {
action: "media_player.play_media";
target?: { entity_id?: string };
entity_id?: string;
data: { media_content_id: string; media_content_type: string };
metadata: Record<string, unknown>;
}
export interface RepeatAction extends BaseAction { export interface RepeatAction extends BaseAction {
repeat: CountRepeat | WhileRepeat | UntilRepeat | ForEachRepeat; repeat: CountRepeat | WhileRepeat | UntilRepeat | ForEachRepeat;
} }
@@ -266,7 +245,6 @@ export type NonConditionAction =
| ChooseAction | ChooseAction
| IfAction | IfAction
| VariablesAction | VariablesAction
| PlayMediaAction
| StopAction | StopAction
| SequenceAction | SequenceAction
| ParallelAction | ParallelAction
@@ -291,7 +269,6 @@ export interface ActionTypes {
wait_for_trigger: WaitForTriggerAction; wait_for_trigger: WaitForTriggerAction;
variables: VariablesAction; variables: VariablesAction;
service: ServiceAction; service: ServiceAction;
play_media: PlayMediaAction;
stop: StopAction; stop: StopAction;
sequence: SequenceAction; sequence: SequenceAction;
parallel: ParallelAction; parallel: ParallelAction;
@@ -398,11 +375,6 @@ export const getActionType = (action: Action): ActionType => {
return "set_conversation_response"; return "set_conversation_response";
} }
if ("action" in action || "service" in action) { if ("action" in action || "service" in action) {
if ("metadata" in action) {
if (is(action, playMediaActionStruct)) {
return "play_media";
}
}
return "service"; return "service";
} }
return "unknown"; return "unknown";
@@ -443,6 +415,31 @@ export const migrateAutomationAction = (
delete action.scene; delete action.scene;
} }
// legacy play media
if (
typeof action === "object" &&
action !== null &&
"action" in action &&
action.action === "media_player.play_media" &&
"data" in action &&
((action.data as any)?.media_content_id ||
(action.data as any)?.media_content_type)
) {
const oldData = { ...(action.data as any) };
const media = {
media_content_id: oldData.media_content_id,
media_content_type: oldData.media_content_type,
metadata: { ...(action.metadata || {}) },
};
delete action.metadata;
delete oldData.media_content_id;
delete oldData.media_content_type;
action.data = {
...oldData,
media,
};
}
if (typeof action === "object" && action !== null && "sequence" in action) { if (typeof action === "object" && action !== null && "sequence" in action) {
for (const sequenceAction of (action as SequenceAction).sequence) { for (const sequenceAction of (action as SequenceAction).sequence) {
migrateAutomationAction(sequenceAction); migrateAutomationAction(sequenceAction);

View File

@@ -26,7 +26,6 @@ import type {
EventAction, EventAction,
IfAction, IfAction,
ParallelAction, ParallelAction,
PlayMediaAction,
RepeatAction, RepeatAction,
SequenceAction, SequenceAction,
SetConversationResponseAction, SetConversationResponseAction,
@@ -303,27 +302,6 @@ const tryDescribeAction = <T extends ActionType>(
}); });
} }
if (actionType === "play_media") {
const config = action as PlayMediaAction;
const entityId = config.target?.entity_id || config.entity_id;
const mediaStateObj = entityId ? hass.states[entityId] : undefined;
return hass.localize(
`${actionTranslationBaseKey}.play_media.description.full`,
{
hasMedia:
config.metadata.title || config.data.media_content_id
? "true"
: "false",
media:
(config.metadata.title as string | undefined) ||
config.data.media_content_id,
hasMediaPlayer:
mediaStateObj || entityId !== undefined ? "true" : "false",
mediaPlayer: mediaStateObj ? computeStateName(mediaStateObj) : entityId,
}
);
}
if (actionType === "wait_for_trigger") { if (actionType === "wait_for_trigger") {
const config = action as WaitForTriggerAction; const config = action as WaitForTriggerAction;
const triggers = ensureArray(config.wait_for_trigger); const triggers = ensureArray(config.wait_for_trigger);

View File

@@ -323,6 +323,7 @@ export interface MediaSelectorValue {
media_class?: string; media_class?: string;
children_media_class?: string | null; children_media_class?: string | null;
navigateIds?: { media_content_type: string; media_content_id: string }[]; navigateIds?: { media_content_type: string; media_content_id: string }[];
browse_entity_id?: string;
}; };
} }

View File

@@ -81,7 +81,6 @@ import "./types/ha-automation-action-device_id";
import "./types/ha-automation-action-event"; import "./types/ha-automation-action-event";
import "./types/ha-automation-action-if"; import "./types/ha-automation-action-if";
import "./types/ha-automation-action-parallel"; import "./types/ha-automation-action-parallel";
import "./types/ha-automation-action-play_media";
import { getRepeatType } from "./types/ha-automation-action-repeat"; import { getRepeatType } from "./types/ha-automation-action-repeat";
import "./types/ha-automation-action-sequence"; import "./types/ha-automation-action-sequence";
import "./types/ha-automation-action-service"; import "./types/ha-automation-action-service";
@@ -96,7 +95,7 @@ export const getAutomationActionType = memoizeOne(
return undefined; return undefined;
} }
if ("action" in action) { if ("action" in action) {
return getActionType(action) as "action" | "play_media"; return getActionType(action) as "action";
} }
if (CONDITION_BUILDING_BLOCKS.some((key) => key in action)) { if (CONDITION_BUILDING_BLOCKS.some((key) => key in action)) {
return "condition" as const; return "condition" as const;

View File

@@ -1,79 +0,0 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-selector/ha-selector";
import type { PlayMediaAction } from "../../../../../data/script";
import type {
MediaSelectorValue,
Selector,
} from "../../../../../data/selector";
import type { HomeAssistant } from "../../../../../types";
import type { ActionElement } from "../ha-automation-action-row";
const MEDIA_SELECTOR_SCHEMA: Selector = {
media: {},
};
@customElement("ha-automation-action-play_media")
export class HaPlayMediaAction extends LitElement implements ActionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public disabled = false;
@property({ attribute: false }) public action!: PlayMediaAction;
@property({ type: Boolean }) public narrow = false;
public static get defaultConfig(): PlayMediaAction {
return {
action: "media_player.play_media",
target: { entity_id: "" },
data: { media_content_id: "", media_content_type: "" },
metadata: {},
};
}
private _getSelectorValue = memoizeOne(
(action: PlayMediaAction): MediaSelectorValue => ({
entity_id: action.target?.entity_id || action.entity_id,
media_content_id: action.data?.media_content_id,
media_content_type: action.data?.media_content_type,
metadata: action.metadata,
})
);
protected render() {
return html`
<ha-selector
.selector=${MEDIA_SELECTOR_SCHEMA}
.hass=${this.hass}
.disabled=${this.disabled}
.value=${this._getSelectorValue(this.action)}
@value-changed=${this._valueChanged}
></ha-selector>
`;
}
private _valueChanged(ev: CustomEvent<{ value: MediaSelectorValue }>) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
...this.action,
action: "media_player.play_media",
target: { entity_id: ev.detail.value.entity_id },
data: {
media_content_id: ev.detail.value.media_content_id,
media_content_type: ev.detail.value.media_content_type,
},
metadata: ev.detail.value.metadata || {},
} as PlayMediaAction,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-play_media": HaPlayMediaAction;
}
}

View File

@@ -244,14 +244,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
manifests?: DomainManifestLookup manifests?: DomainManifestLookup
): ListItem[] => { ): ListItem[] => {
if (type === "action" && isService(group)) { if (type === "action" && isService(group)) {
let result = this._services(localize, services, manifests, group); return this._services(localize, services, manifests, group);
if (group === `${SERVICE_PREFIX}media_player`) {
result = [
this._convertToItem("play_media", {}, type, localize),
...result,
];
}
return result;
} }
const groups = this._getGroups(type, group); const groups = this._getGroups(type, group);

View File

@@ -452,7 +452,9 @@
"browse_media": "Browse media", "browse_media": "Browse media",
"manual": "Manually enter media ID", "manual": "Manually enter media ID",
"media_content_id": "Media content ID", "media_content_id": "Media content ID",
"media_content_type": "Media content type" "media_content_type": "Media content type",
"media_content_id_detail": "The ID of the content to play. Platform dependent.",
"media_content_type_detail": "The type of the content to play, such as image, music, tv show, video, episode, channel, or playlist."
}, },
"file": { "file": {
"upload_failed": "Upload failed", "upload_failed": "Upload failed",