From 673ca8ba4b0a75be3f328e27d7211f0e30342238 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Wed, 27 Aug 2025 03:39:40 -0700 Subject: [PATCH] Use media selector for media_player.play_media (#26559) --- gallery/src/pages/automation/editor-action.ts | 2 - .../ha-selector/ha-selector-media.ts | 76 ++++++++++++++---- src/data/script.ts | 53 ++++++------- src/data/script_i18n.ts | 22 ------ src/data/selector.ts | 1 + .../action/ha-automation-action-row.ts | 3 +- .../types/ha-automation-action-play_media.ts | 79 ------------------- .../add-automation-element-dialog.ts | 9 +-- src/translations/en.json | 4 +- 9 files changed, 91 insertions(+), 158 deletions(-) delete mode 100644 src/panels/config/automation/action/types/ha-automation-action-play_media.ts diff --git a/gallery/src/pages/automation/editor-action.ts b/gallery/src/pages/automation/editor-action.ts index eca218d1b4..edfdec092c 100644 --- a/gallery/src/pages/automation/editor-action.ts +++ b/gallery/src/pages/automation/editor-action.ts @@ -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 { 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 { 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 { 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"; @@ -32,7 +31,6 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [ { name: "Service", actions: [HaServiceAction.defaultConfig] }, { name: "Condition", actions: [HaConditionAction.defaultConfig] }, { name: "Delay", actions: [HaDelayAction.defaultConfig] }, - { name: "Play media", actions: [HaPlayMediaAction.defaultConfig] }, { name: "Wait", actions: [HaWaitAction.defaultConfig] }, { name: "WaitForTrigger", actions: [HaWaitForTriggerAction.defaultConfig] }, { name: "Repeat", actions: [HaRepeatAction.defaultConfig] }, diff --git a/src/components/ha-selector/ha-selector-media.ts b/src/components/ha-selector/ha-selector-media.ts index 3f8b229d9f..43632feb85 100644 --- a/src/components/ha-selector/ha-selector-media.ts +++ b/src/components/ha-selector/ha-selector-media.ts @@ -18,6 +18,7 @@ import "../ha-alert"; import "../ha-form/ha-form"; import type { SchemaUnion } from "../ha-form/types"; import { showMediaBrowserDialog } from "../media-player/show-media-browser-dialog"; +import { ensureArray } from "../../common/array/ensure-array"; const MANUAL_SCHEMA = [ { 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({ attribute: false }) public context?: { + filter_entity?: string | string[]; + }; + @state() private _thumbnailUrl?: string | null; + private _contextEntities: string[] | undefined; + willUpdate(changedProps: PropertyValues) { + if (changedProps.has("context")) { + this._contextEntities = ensureArray(this.context?.filter_entity); + } + if (changedProps.has("value")) { const thumbnail = this.value?.metadata?.thumbnail; const oldThumbnail = (changedProps.get("value") as this["value"]) @@ -79,24 +90,25 @@ export class HaMediaSelector extends LitElement { } protected render() { - const stateObj = this.value?.entity_id - ? this.hass.states[this.value.entity_id] - : undefined; + const entityId = this._getActiveEntityId(); + + const stateObj = entityId ? this.hass.states[entityId] : undefined; const supportsBrowse = - !this.value?.entity_id || + !entityId || (stateObj && supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)); const hasAccept = this.selector?.media?.accept?.length; return html` - ${hasAccept + ${hasAccept || + (this._contextEntities && this._contextEntities.length <= 1) ? nothing : html` `} @@ -121,6 +135,7 @@ export class HaMediaSelector extends LitElement { .data=${this.value || EMPTY_FORM} .schema=${MANUAL_SCHEMA} .computeLabel=${this._computeLabelCallback} + .computeHelper=${this._computeHelperCallback} > ` : html` @@ -133,7 +148,7 @@ export class HaMediaSelector extends LitElement { : this.value.metadata?.title || this.value.media_content_id} @click=${this._pickMedia} @keydown=${this._handleKeyDown} - class=${this.disabled || (!this.value?.entity_id && !hasAccept) + class=${this.disabled || (!entityId && !hasAccept) ? "disabled" : ""} > @@ -193,21 +208,38 @@ export class HaMediaSelector extends LitElement { ): string => this.hass.localize(`ui.components.selectors.media.${schema.name}`); + private _computeHelperCallback = ( + schema: SchemaUnion + ): string => + this.hass.localize(`ui.components.selectors.media.${schema.name}_detail`); + private _entityChanged(ev: CustomEvent) { ev.stopPropagation(); - fireEvent(this, "value-changed", { - value: { - entity_id: ev.detail.value, - media_content_id: "", - media_content_type: "", - }, - }); + if (this.context?.filter_entity) { + fireEvent(this, "value-changed", { + value: { + media_content_id: "", + 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() { showMediaBrowserDialog(this, { action: "pick", - entityId: this.value?.entity_id, + entityId: this._getActiveEntityId(), navigateIds: this.value?.metadata?.navigateIds, accept: this.selector.media?.accept, mediaPickedCallback: (pickedMedia: MediaPickedEvent) => { @@ -225,6 +257,9 @@ export class HaMediaSelector extends LitElement { media_content_type: id.media_content_type, 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) { if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); diff --git a/src/data/script.ts b/src/data/script.ts index c7ba8d6c79..aa3efc4437 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -11,8 +11,6 @@ import { union, array, assign, - literal, - is, boolean, refine, } from "superstruct"; @@ -68,17 +66,6 @@ export const serviceActionStruct: Describe = assign( }) ); -const playMediaActionStruct: Describe = 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 { attributes: HassEntityAttributeBase & { last_triggered: string; @@ -182,14 +169,6 @@ export interface WaitForTriggerAction extends BaseAction { 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; -} - export interface RepeatAction extends BaseAction { repeat: CountRepeat | WhileRepeat | UntilRepeat | ForEachRepeat; } @@ -266,7 +245,6 @@ export type NonConditionAction = | ChooseAction | IfAction | VariablesAction - | PlayMediaAction | StopAction | SequenceAction | ParallelAction @@ -291,7 +269,6 @@ export interface ActionTypes { wait_for_trigger: WaitForTriggerAction; variables: VariablesAction; service: ServiceAction; - play_media: PlayMediaAction; stop: StopAction; sequence: SequenceAction; parallel: ParallelAction; @@ -398,11 +375,6 @@ export const getActionType = (action: Action): ActionType => { return "set_conversation_response"; } if ("action" in action || "service" in action) { - if ("metadata" in action) { - if (is(action, playMediaActionStruct)) { - return "play_media"; - } - } return "service"; } return "unknown"; @@ -443,6 +415,31 @@ export const migrateAutomationAction = ( 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) { for (const sequenceAction of (action as SequenceAction).sequence) { migrateAutomationAction(sequenceAction); diff --git a/src/data/script_i18n.ts b/src/data/script_i18n.ts index af1ebe2713..7d99a803ff 100644 --- a/src/data/script_i18n.ts +++ b/src/data/script_i18n.ts @@ -26,7 +26,6 @@ import type { EventAction, IfAction, ParallelAction, - PlayMediaAction, RepeatAction, SequenceAction, SetConversationResponseAction, @@ -303,27 +302,6 @@ const tryDescribeAction = ( }); } - 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") { const config = action as WaitForTriggerAction; const triggers = ensureArray(config.wait_for_trigger); diff --git a/src/data/selector.ts b/src/data/selector.ts index 880178bdba..00ff98e321 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -323,6 +323,7 @@ export interface MediaSelectorValue { media_class?: string; children_media_class?: string | null; navigateIds?: { media_content_type: string; media_content_id: string }[]; + browse_entity_id?: string; }; } diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts index 4433892287..4e82501a89 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -81,7 +81,6 @@ import "./types/ha-automation-action-device_id"; import "./types/ha-automation-action-event"; import "./types/ha-automation-action-if"; import "./types/ha-automation-action-parallel"; -import "./types/ha-automation-action-play_media"; import { getRepeatType } from "./types/ha-automation-action-repeat"; import "./types/ha-automation-action-sequence"; import "./types/ha-automation-action-service"; @@ -96,7 +95,7 @@ export const getAutomationActionType = memoizeOne( return undefined; } 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)) { return "condition" as const; diff --git a/src/panels/config/automation/action/types/ha-automation-action-play_media.ts b/src/panels/config/automation/action/types/ha-automation-action-play_media.ts deleted file mode 100644 index 0b77eba251..0000000000 --- a/src/panels/config/automation/action/types/ha-automation-action-play_media.ts +++ /dev/null @@ -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` - - `; - } - - 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; - } -} diff --git a/src/panels/config/automation/add-automation-element-dialog.ts b/src/panels/config/automation/add-automation-element-dialog.ts index 244500a052..222778872a 100644 --- a/src/panels/config/automation/add-automation-element-dialog.ts +++ b/src/panels/config/automation/add-automation-element-dialog.ts @@ -244,14 +244,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog { manifests?: DomainManifestLookup ): ListItem[] => { if (type === "action" && isService(group)) { - let result = this._services(localize, services, manifests, group); - if (group === `${SERVICE_PREFIX}media_player`) { - result = [ - this._convertToItem("play_media", {}, type, localize), - ...result, - ]; - } - return result; + return this._services(localize, services, manifests, group); } const groups = this._getGroups(type, group); diff --git a/src/translations/en.json b/src/translations/en.json index cdc1a1f3de..6df9c03d21 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -452,7 +452,9 @@ "browse_media": "Browse media", "manual": "Manually enter media 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": { "upload_failed": "Upload failed",